Zoho Books – Gotchas

Service ID: zoho-books

Zoho Books is cloud-based accounting software in which you can record, audit and analyze all financial transactions easily. With secure data storage, easy navigation and customizable features, Zoho Books provide a head start in accounting for small businesses.

⚠️

45 gotchas across 23 resources

These are connector-specific behaviors and limitations to be aware of when integrating.

oneagedCreditorsOne

For vendors with an exceptionally large number of open bills, report generation may take longer since Zoho returns bills in batches of 200 and every batch is fetched before the report is built. Use filter[supplier_id] to scope the report to a single supplier if this becomes noticeable.

Aged Debtors1 gotcha

oneagedDebtorsOne

For customers with an exceptionally large number of open invoices, report generation may take longer since Zoho returns invoices in batches of 200 and every batch is fetched before the report is built. Use filter[customer_id] to scope the report to a single customer if this becomes noticeable.

Attachments1 gotcha

allattachmentsAll

Zoho Books has no standalone attachments-listing endpoint; attachment metadata is nested inside each record's own detail response as a documents array. reference_id here is the record's own unified id (no separate lookup needed) for invoice, bill, expense, and journal-entry, and credit-note. The credit-note list route is not live-verified — if Zoho's real credit-note detail response omits the documents array (unconfirmed either way), listing returns an empty result rather than an error; upload is unaffected either way.

onebalanceSheetOne

Zoho Books does not publish a documented reporting API for this report. This connector reads it from a working but undocumented endpoint, which may change without notice. Only the date filters listed for this connector are supported: accounting method, period count, period type and location filters are not available.

Accounts with a zero balance as of the requested date are omitted from the response entirely rather than returned with a zero value.

allbankAccountsAll

Zoho only has two account types, bank and credit_card. On write, savings, money_market, other and cash are all clamped to bank, and line_of_credit is clamped to credit_card — there is no dedicated Zoho type for any of them. On read, both clamp targets read back as the single closest unified value (checking for bank, credit_card for credit_card), so a write/read round-trip through one of the clamped values does not return the original value.

status is synthesized from Zoho's is_active boolean (active/inactive) — Zoho has no closed state.

allbankFeedAccountsAll

Listing Bank Feed Accounts is not supported. Zoho Books has no separate registry of feed connections — a bank feed account IS the underlying bank account. Use the Bank Accounts endpoint to find the account you need, then pass its id as bank_feed_account_id when creating bank feed statements.

addbankFeedAccountsAdd

This operation always creates a new bank account in Zoho Books — Zoho has no concept of registering an existing account as a separate feed object, so target_account_id is not used. target_account_name is required even though it is not strict-required by this schema; omitting it returns "Invalid value passed for Account Name". target_account_number and currency are optional. source_account_id is not persisted — reads return it equal to the account's own id; source_account_number and country are not persisted either.

target_account_number is read back masked (for example xxxx1234), and is dropped entirely for a credit_card account type — Zoho does not store an account number for that account type. source_routing_number is persisted and returned as submitted.

To attach a feed to an account that already exists in Zoho Books, skip this create call: find it with the Bank Accounts endpoint and pass its id as bank_feed_account_id when creating bank feed statements.

onebankFeedAccountsOne

Zoho Books has no separate feed-connection object: this returns the same account data as bank-accounts.one, addressed by the same id. bankFeedAccountsAll/Add are both unsupported — use bank-accounts.all/.one to discover or create the underlying account instead.

feed_status always reads as pending — Zoho has no concept of feed-delivery state. source_account_id always equals target_account_id — Zoho has no separate source/aggregator-side account identity, so both fields are always populated with the same value.

allbankFeedStatementsAll

Zoho has no endpoint that lists bank feed statements directly — this operation lists bank/credit card accounts and reads back each one's single most-recently-imported statement, keeping only the accounts that have one. This is one downstream call per account, so response time scales with the number of accounts on the connection. limit/cursor paginate the underlying account scan, not the statements returned — a page can come back with data: [] and a non-null next cursor while later pages still hold results, if the accounts on that page happen to have no statement. Keep following next until it is null rather than stopping at the first empty page.

id is a composite of the account and statement (account_id:statement_id) — Zoho has no statement id that is unique on its own across accounts. Zoho keeps only the single most-recently-imported statement live per account: deleting an account's only import leaves it with none (bank-feed-statements.one/.get then 404s for that account, the same as an account that never had an import). Deleting the newer of two or more imports on the same account instead reveals the one before it — imports on an account with more than one behave as a stack, not a discarded history.

start_balance/end_balance are never populated — Zoho does not track statement-level balances for an imported statement, only the transaction lines themselves. start_date and end_date reflect the range Zoho itself derives from the transaction dates, not any date range supplied on create.

status always reads as pending: every transaction line created through this endpoint starts out uncategorized in Zoho and stays that way until a person or rule categorizes it inside Zoho's own UI, which this connector does not track. A transaction Zoho treats as a duplicate of one already on the account is silently excluded from the statement it was submitted in — nothing in the response distinguishes a partially-imported statement from a fully-imported one. Supplying a distinct transactions[].reference per line avoids this for genuinely distinct transactions that would otherwise look identical to Zoho (same date, amount and direction).

transactions[].description is accepted on create but is not returned by Zoho's read endpoint for this record type — it reads back empty. Zoho inverts the direction of transactions[].credit_or_debit between what is submitted and what is read back for this specific endpoint; the value returned here is the corrected, intended direction.

transactions[].source_transaction_id is required on create but Zoho has no field to store a caller-supplied transaction identifier — the value returned on read is Zoho's own generated transaction id, not the one originally submitted. transactions[].amount is always read back as a positive magnitude regardless of the sign submitted; direction comes only from credit_or_debit.

Creating two statements on the same account concurrently is not safe: Zoho exposes only the single current import per account with no way to correlate a create call back to its own result, so a caller in that situation may receive another caller's statement id.

addbankFeedStatementsAdd

Every transaction in transactions is imported as a genuinely uncategorized line awaiting reconciliation in Zoho — not posted as a categorized transaction, and no connection configuration is required to use this operation. Multiple transactions per call are supported.

status, start_balance, end_balance, start_date/end_date and their credit/debit indicators are not sent — Zoho computes the statement's date range from the transaction dates itself and has no field for statement-level balances. transactions[].description and transactions[].source_transaction_id are accepted but not returned by Zoho's read endpoint for this record type; transactions[].counterparty and .reference are sent and do round-trip. Zoho inverts the direction of transactions[].credit_or_debit between what is submitted and what is read back — see the list operation's gotcha above for the corrected behaviour on read.

A transaction that looks identical to one Zoho already has for the account (same date, amount and direction) is silently excluded from the import rather than rejected — give genuinely distinct same-day/same-amount transactions a distinct reference to avoid this. Do not call this operation concurrently for the same bank_feed_account_id — see the list operation's gotcha above for why.

The response id is a composite of the account and the statement Zoho assigned (account_id:statement_id) — Zoho's own create response carries no id to return directly, so this operation makes an immediate follow-up read to learn it. If that follow-up read itself fails, the response is a 500 even though the statement was already imported — Zoho has no idempotency key for this endpoint, so retrying the same create in that situation risks importing the transactions a second time rather than recovering the id of the first attempt.

onebankFeedStatementsOne

Zoho tracks only the single most-recently-imported statement per account — this operation can only ever return the account's current statement, never an older one. Requesting an id whose statement has since been superseded by a newer import returns a 404 that looks identical to an id that never existed — but it is not necessarily permanent: deleting the newer statement makes the older one's id resolve again, since imports behave as a stack (last in, first out), not a discarded history.

deletebankFeedStatementsDelete

Only the account's current (most-recently-imported) statement can be deleted — an id for a statement that has already been superseded by a newer import returns a 400, not a 404. Deleting the current statement makes the previous one, if any, become current again and resolve through bank-feed-statements.one/.get.

addbillCreditNotesAdd

Vendor credits have a draft → open → void lifecycle. A new credit is created in draft; set status to authorised to open it (submit) or voided to void it — updating status transitions the credit rather than editing a free-form field. balance and remaining_credit both report the unapplied amount, and the document number (number) is auto-generated with a DN prefix when omitted on create.

allbillPaymentsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

Bills3 gotchas

allbillsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

addbillsAdd

The status of the bill will be "submitted" by default when creating a bill.

updatebillsUpdate

The allowed values for the status field are "draft", "submitted" and "void" when using the update method. Please send status updates separately from other bill updates.

Credit Notes3 gotchas

allcreditNotesAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

addcreditNotesAdd

By default, the status of the bill will be set to "authorised".

updatecreditNotesUpdate

Allowed values for the status are "draft", "authorised", and "voided" when using the update method. Please send status updates separately from other updates for Credit Notes.

Customers3 gotchas

allcustomersAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

allcustomersAll

balance is the customer's full outstanding receivable total — their opening balance plus any unpaid invoices, minus payments applied — in the customer's currency. It does not include unapplied credits (Zoho Books' separate unused_credits_receivable_amount), and is populated on both this list endpoint and the single-customer read with no extra parameter required.

updatecustomersUpdate

The status field can be updated to either "active" or "inactive". Please send status updates separately from other updates for the customer.

Expenses4 gotchas

allexpensesAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

addexpensesAdd

Zoho Books supports only one customer and one rebilling (rebillable) flag per expense at the header level. Only provide these in the first line_item — they will be mapped to the header-level fields and applied to all lines. If differentiated treatment is required, create separate expenses.

oneexpensesOne

Zoho Books supports only one customer and one rebilling (rebillable) flag per expense at the header level. When reading, all line_items will return the same customer and rebilling values.

updateexpensesUpdate

Zoho Books supports only one customer and one rebilling (rebillable) flag per expense at the header level. Only provide these in the first line_item — they will be mapped to the header-level fields and applied to all lines. If differentiated treatment is required, create separate expenses.

Invoice Items2 gotchas

allinvoiceItemsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

updateinvoiceItemsUpdate

Please ensure that status updates are sent separately from other invoice item updates.

Invoices3 gotchas

allinvoicesAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

addinvoicesAdd

By default, the status of the invoice will be set to "draft".

updateinvoicesUpdate

The allowed values for the status field are "draft", "submitted", and "void" when using the update method. Please ensure that status updates are sent separately from other invoice updates.

alljournalEntriesAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

Journal entries are returned in Zoho's internal entry-number order, newest created first — not by posting date. Zoho's own default sorts by journal_date, which has no unique tiebreaker, and paginating over it means a page boundary falling inside a group of same-dated entries silently duplicates some records and skips others. Entry number is unique per organization, so pagination over it is stable. In practice the two orders are close, because entry numbers are assigned in creation order; they diverge for back-dated entries, where an entry created today for an earlier period sorts first rather than by its posted_at.

To sort by posting date instead, pass ?pass_through[sort_column]=journal_date&pass_through[sort_order]=D — but note this reinstates the duplicate-and-skip behaviour at page boundaries, so only use it for a single page rather than when paginating a full ledger.

Zoho Books omits line_items from the journal list endpoint, so each list request triggers one additional detail fetch per journal to hydrate them. A 100-row page issues 100 detail calls in bounded concurrent batches of 5 (to fit Zoho's 100/min per-org and 5-concurrent Free-tier limits). Detail failures for individual journals are isolated: the affected record is returned with line_items: [] and a warning surfaced, while the rest of the page returns normally. To reduce call volume, use limit to bound the page size.

filter[updated_since] is applied at day granularity: Zoho only supports date-level last-modified filtering, and the comparison happens in the organization's local timezone. The filter is widened one day back from the given timestamp so no modified record is ever missed, which means results can include records last modified up to two days before the requested instant. Deduplicate on updated_at client-side if exact cut-off matters.

filter[status] supports only draft and posted (Zoho journals have no other states). Any other status value is rejected by Zoho with "Invalid value passed for status".

filter[start_date] / filter[end_date] filter on the journal posting date and are inclusive on both ends.

allledgerAccountsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

oneledgerAccountsOne

current_balance on a single ledger account is sourced from Zoho's closing_balance, which differs from the list endpoint's balance in three ways: it is an absolute value (the list returns a signed balance, negative when the balance sits on the account's non-normal side), it includes future-dated transactions, and system-computed accounts (e.g. Retained Earnings) return 0. For signed, as-of-today balances across accounts, use the list endpoint (GET /accounting/ledger-accounts).

updateledgerAccountsUpdate

For ledger accounts, please update the "active" status separately from other updates.

Payments1 gotcha

allpaymentsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

oneprofitAndLossOne

Zoho Books does not publish a documented reporting API for this report. This connector reads it from a working but undocumented endpoint, which may change without notice. Only the date filters listed for this connector are supported: accounting method, period count, period type and location filters are not available.

Both filter[start_date] and filter[end_date] are required together. Omitting either one returns an error rather than a default period.

Accounts with a zero balance in the requested period are omitted from the response entirely rather than returned with a zero total.

Projects1 gotcha

allprojectsAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

allpurchaseOrdersAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

addpurchaseOrdersAdd

By default, the status of the purchase order will be set to "draft".

updatepurchaseOrdersUpdate

Allowed values for the status are "draft", "open", "billed" and "deleted" when using the update method. Please send status updates separately from other updates for the supplier.

Suppliers2 gotchas

allsuppliersAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.

updatesuppliersUpdate

The status field can be updated to either "active" or "inactive". Please send status updates separately from other updates for the supplier.

Tax Rates1 gotcha

alltaxRatesAll

Zoho Books does not expose a total record count in list responses. meta.total_count will not appear.