# Zoho Books — Accounting API: gotchas

> Canonical URL: https://developers.apideck.com/apis/accounting/zoho-books/gotchas

## GET /accounting/tax-rates

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

## GET /accounting/bills

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

## POST /accounting/bills

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

## PATCH /accounting/bills/{id}

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.

## POST /accounting/bills/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N bills takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/invoices

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

## POST /accounting/invoices

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

## POST /accounting/invoices

`quotes` links the invoice to an existing Zoho Books estimate: send the quote's `id`
(`number` is read-only and is not sent). Zoho Books links one estimate per invoice, so only
the first quote in `quotes` is sent. The estimate must belong to the invoice's customer: a
quote `id` that does not exist, or that belongs to another customer, fails the request.
Zoho Books does not check the estimate's status (draft and declined estimates are linked
too), and one estimate can be linked to more than one invoice. Linking does not copy the
estimate's lines onto the invoice; send `line_items` yourself. Linking marks the estimate as
invoiced in Zoho Books.

## PATCH /accounting/invoices/{id}

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.

## POST /accounting/invoices/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N invoices takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/ledger-accounts

Zoho Books does not expose a total record count in list responses. `meta.total_count` will not appear.
Fixed assets: the `fixed_asset` object comes from Zoho Books' fixed-asset register, for an account that holds asset cost: the active and fully depreciated assets whose asset account it is, matched by account name (unique in Zoho Books). `fixed_asset.cost` and `fixed_asset.accumulated_depreciation` are totals over those assets, accumulated depreciation being each asset's cost minus its current value; they come from the register, not the ledger, so they can differ from `current_balance`. An account usually holds several assets, so `acquisition_date`, `depreciation_method`, `downstream_depreciation_method` and `useful_life_months` are set only when every one of its assets has the same value; methods are compared as Zoho Books names them, so `150_declining_method` and `200_declining_method` differ. The object needs a Premium plan with the Fixed Asset module enabled; otherwise it is not set. It is computed only when `fields` names `fixed_asset` or one of its sub-fields (for example `fields=id,name,current_balance,fixed_asset`) and the connection was authorized with a fixed-asset scope (`ZohoBooks.fullaccess.all`, the default, or `ZohoBooks.fixedasset.*`); it then costs one extra request per 200 assets in the register. With Data Scopes, name the consented sub-fields (`fixed_asset.cost`, and so on). With more than 2,000 assets, or when reading the register takes past 20 seconds of the request, it is not set. Accumulated-depreciation accounts and other accounts have no `fixed_asset`.

## GET /accounting/ledger-accounts/{id}

`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`).

The `fixed_asset` object comes from the fixed-asset register, as described on the list:
computed only when `fields` names `fixed_asset` or one of its sub-fields.

## PATCH /accounting/ledger-accounts/{id}

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

## POST /accounting/ledger-accounts/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N ledger accounts takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/invoice-items

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

## PATCH /accounting/invoice-items/{id}

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

## GET /accounting/credit-notes

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

## POST /accounting/credit-notes

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

## PATCH /accounting/credit-notes/{id}

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.

## POST /accounting/credit-notes/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N credit notes takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## POST /accounting/bill-credit-notes

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.

## POST /accounting/bill-credit-notes/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N bill credit notes takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/customers

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

## GET /accounting/customers

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

## PATCH /accounting/customers/{id}

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

## POST /accounting/customers/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N customers takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/suppliers

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

## PATCH /accounting/suppliers/{id}

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

## POST /accounting/suppliers/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N suppliers takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/payments

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

## POST /accounting/payments/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N payments takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/balance-sheet

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.

## GET /accounting/profit-and-loss

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.

## GET /accounting/journal-entries

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.

## POST /accounting/journal-entries/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N journal entries takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/general-ledger-transactions

Built from Zoho Books' Account Transactions report, which is not part of Zoho's published
API reference. Every posting is included: invoices, payments, credit notes, expenses,
manual journals and the rest. Zoho's documented account-transactions endpoint only returns
per-entity counts, so it is not used.

The list covers the organization's full history by default (postings dated from
1900-01-01 to 2099-12-31). To narrow it, pass
`pass_through[from_date]` and `pass_through[to_date]` (`YYYY-MM-DD`, posting date,
inclusive). Pass `pass_through[cash_based]=true` for cash-basis postings instead of accrual.
`filter[...]` is not supported, and a `pass_through` override of `sort_column`,
`sort_order`, `group_by`, `page` or `per_page` is rejected with a 400.

`limit` counts ledger lines, not transactions. Each page returns whole transactions, so it
can hold fewer than `limit` records, or none, while `next` is still set: keep following
`next` until it is `null`. To return whole transactions across page boundaries, a page can
also read the previous page and every line of its first and last posting dates, so a page
usually costs one to four Zoho calls (one more per extra 200 lines on those dates, so at
most 22), plus an organization lookup for `currency` at most every few minutes, each counted
as a request against both Unify and the organization's Zoho API quota. If one of those reads
fails, or the lines on the page's first or last posting date no longer match between reads,
the page is not returned: the request fails with a 502 (or 429 when Zoho rate-limits it) and
can be retried. Changes to other dates while a caller is paging are not detected: lines
added or removed on earlier dates shift Zoho's pages, as in any offset-paged list. If Zoho
rejects one of those reads (for example expired credentials), the request fails with Zoho's
4xx status instead. A single posting date with more than 2,000 ledger lines cannot be paged
and always returns a 502, as does a page whose lines repeat identically within a date and
cannot be placed; a larger `limit` avoids the latter.

A Zoho document can post on more than one date, for example a customer payment received in
advance and later applied to an invoice. Each date is returned as its own balanced
transaction: `id` is `<Zoho document id>-<posting date>` and `source_id` is the Zoho
document id. Because the posting date is part of `id`, editing a document's date in Zoho
changes the `id` of its transaction: a sync sees the old transaction disappear and a new
one appear, and should treat them as a removal and an addition.

Amounts are in the organization's base currency, which is what `currency` holds.
`ledger_account.code`, line ids and tax details are not returned. `number` is Zoho's entity
number on the transaction's first line; on a payment's application to an invoice it is the
invoice number. A line's `description` is Zoho's "Transaction Details" column: the customer
or vendor for sales, purchases and payments, the other account for transfers, and the notes
for manual journals.

`source_type` maps invoices to `invoice`, bills to `bill`, credit notes and vendor credits
to `credit_note`, customer and vendor payments to `payment`, refunds to `refund`, expenses
to `expense` and manual journals to `journal_entry`. Every other Zoho transaction type
(sales returns, transfers, deposits, card payments, owner drawings and contributions, other
income, opening balances, ...) is returned as `other`. A vendor credit's `source_id` is read
through `bill-credit-notes`, not `credit-notes`.

Reading a single transaction by `id` is not supported.

## GET /accounting/purchase-orders

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

## POST /accounting/purchase-orders

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

## PATCH /accounting/purchase-orders/{id}

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.

## GET /accounting/attachments/{reference_type}/{reference_id}

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.

## GET /accounting/bank-accounts

This list includes every bank account, petty cash and deactivated accounts among them.
`bank-feed-accounts` returns only the accounts that can receive a feed, so the two lists are
expected to differ in length.

`account_type` reads back as `checking`, `credit_card` or `cash`, and `status` as `active` or
`inactive`.

## POST /accounting/bank-accounts

`account_type` is clamped to what Zoho Books stores: `savings`, `money_market` and `other` are
created as `checking`, and `line_of_credit` as `credit_card`, so an account created through one
of those does not read back as the value sent. `cash` and `credit_card` are stored as sent.

`status` is ignored on create — use the update operation to deactivate an account afterwards.
`account_number` reads back masked, and is not stored for a `credit_card` account.

## GET /accounting/bank-accounts/{id}

`account_number` reads back masked (for example `xxxx1234`), and is not returned at all for a
`credit_card` account. The value sent on create cannot be read back, so keep your own copy if
you need it.

## PATCH /accounting/bank-accounts/{id}

`status` accepts `active` and `inactive` only. `closed` is rejected rather than treated as
`inactive`.

## DELETE /accounting/bank-accounts/{id}

Deleting an account also deletes any bank feed transactions still awaiting reconciliation on
it, with no warning. An account holding posted transactions is refused, but staged feed
transactions are not posted transactions, so that protection does not cover them. Read
`bank-feed-statements` for the account first if that matters.

## GET /accounting/trial-balance

Zoho Books reads the trial balance from an undocumented report endpoint (it is not in Zoho's published API reference; the balance sheet and profit and loss use the same kind of endpoint), which Zoho can change without notice. The report is as of `filter[end_date]` (today by default): balance sheet accounts are cumulative and income and expense accounts are year-to-date for the fiscal year that contains it, with earlier years' earnings in the Retained Earnings line. Zoho's trial balance leaves that account's id empty, so Apideck reads it from Zoho's balance sheet report as of the same date (one extra request); if that request fails, or the balance sheet does not identify the account, `account_id` on that line is null and the response carries a warning. Every line has `line_type` `account`, and accounts with a zero balance are left out. `filter[accounting_method]=cash` returns Zoho's cash-basis report; accrual is the default. The lines must add up to Zoho's own report total, and a request whose explicit `filter[end_date]` Zoho answers for a different date fails, both with a `502`; so does a report whose debits and credits differ by half a cent or more. `start_date` is null, and `code` is null for accounts without one.
## POST /accounting/tracking-categories/batch

Creating tracking categories is not supported on Zoho Books.

## GET /accounting/bill-payments

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

## POST /accounting/bill-payments/batch

Records are written one at a time, so the request counts as **one request per
record** against your plan — batching saves you round trips, not quota.

At most 10 records per request.

Records are written one at a time, so a request of N bill payments takes about N times as
long as a single write and counts as N requests against your plan. Each record
succeeds or fails on its own; read the per-item `status` to see which were written.

## GET /accounting/expenses

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

## POST /accounting/expenses

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.
## GET /accounting/expenses/{id}

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.
## PATCH /accounting/expenses/{id}

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.
## GET /accounting/aged-creditors

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.

## GET /accounting/aged-debtors

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.

## GET /accounting/bank-feed-accounts

Returns only the accounts that can receive a feed: active accounts of type bank or credit card.
Petty cash accounts cannot receive one and are excluded, as are deactivated accounts; both
remain available through `bank-accounts`.

Pass an account's `id` as `bank_feed_account_id` when creating bank feed statements.

## POST /accounting/bank-feed-accounts

On ledgers that have one, a bank feed account is a connection registered against an account.
Zoho Books has no such object: the feed account is the bank account itself, so this
operation registers nothing — it creates a bank account. Use `bank-accounts.add`, which is
the operation for that, and which additionally sets `bank_name` and `description` and maps
`account_type` onto what Zoho Books stores.

This operation always creates a new account: `target_account_id` is ignored, so an account
that already exists cannot be attached to a feed through it. To feed an existing account,
skip the create and pass its `id` from `bank-feed-accounts` as `bank_feed_account_id` when
creating statements.

`target_account_name` is required in practice, although this schema does not mark it so.
`source_account_id`, `source_account_number` and `country` are not stored and are not
returned. `target_account_number` reads back masked, and is not stored for a `credit_card`
account.

## GET /accounting/bank-feed-accounts/{id}

`source_account_id` is not returned: there is no aggregator-side account behind a Zoho Books
bank feed. `target_account_id` repeats the account's own `id`, and `feed_status` always reads
`pending` — Zoho Books has no feed-delivery state.

`bank-accounts` returns the same account with its full field set, including `name`,
`account_type`, `bank_name`, `description`, `status` and `available_balance`.

## PATCH /accounting/bank-feed-accounts/{id}

Not supported: a bank feed account is the bank account itself, so there is nothing separate to
change. Use `bank-accounts.update`, which accepts the full field set.

## DELETE /accounting/bank-feed-accounts/{id}

Not supported: a bank feed account is the bank account itself, so deleting one would delete the
account. Use `bank-accounts.delete` if that is the intent, and read its limitation first — it
also removes transactions still awaiting reconciliation.

## GET /accounting/bank-feed-statements

Each statement is a bank account's current set of transactions awaiting reconciliation, and its
`id` is that account's own id. Accounts with nothing awaiting reconciliation are absent from the
list rather than returned empty. `filter[bank_feed_account_id]` narrows the list to one account.

Unlike most list operations, `transactions` is included on every statement.

`status` always reads `pending`. `start_balance` and `end_balance` are never populated.
`start_date` and `end_date` are the earliest and latest transaction date currently awaiting
reconciliation, not a range supplied on create.

## POST /accounting/bank-feed-statements

Transactions are imported awaiting reconciliation, not posted to the ledger, and multiple
transactions per call are supported. The response `id` is the `bank_feed_account_id` supplied,
so two imports against one account return the same id.

Not every field survives. `transactions[].source_transaction_id` and `.merchant_category_code`
are accepted but not returned, `.posted_date` keeps the date and drops the time of day, and
`.transaction_type` reads back derived from the direction rather than as sent. `.description`,
`.counterparty`, `.reference`, `.amount` and `.credit_or_debit` round-trip unchanged, which
makes `reference` the only caller-supplied value available for correlation.

Send `.amount` as a positive number and let `.credit_or_debit` carry the direction — a sign on
the amount is ignored in favour of `.credit_or_debit`. A zero amount is rejected. `status`,
`start_balance`, `end_balance` and `start_date`/`end_date` are not sent; the statement's date
range is derived from the transactions themselves.

A statement is imported in the bank account's own currency. A per-transaction currency is
ignored rather than rejected, so send each currency to its own account.

Imports are not deduplicated: sending the same statement twice imports every transaction twice,
and no caller-supplied identifier can prevent it. Within a single import, two transactions
identical on every field are collapsed into one without warning — giving them distinct
`reference` values keeps them apart.

## GET /accounting/bank-feed-statements/{id}

The `id` is the bank account's own id, and the statement is that account's current set of
transactions awaiting reconciliation — not a stored copy of an import. It includes transactions
entered in Zoho Books directly and excludes everything already reconciled, so an account that
has been worked through returns `200` with an empty `transactions` array rather than a `404`.

`transactions[].source_transaction_id` is not stored and reads back absent. Correlate on
`transactions[].reference`, which round-trips unchanged.

## DELETE /accounting/bank-feed-statements/{id}

Removes the account's most recent import, whichever `id` is passed; call it again to remove the
one before that. Individual transactions cannot be removed. An account with nothing awaiting
reconciliation is answered with a 400 naming the account.

## GET /accounting/projects

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


[API reference](https://developers.apideck.com/apis/accounting/reference.md) · [Coverage matrix](https://developers.apideck.com/apis/accounting/coverage)
