# Dualentry — Accounting API: gotchas

> Canonical URL: https://developers.apideck.com/apis/accounting/dualentry/gotchas

## GET /accounting/tax-rates

DualEntry returns the Product Tax Code catalog (e.g. `TAXABLE - System`, `NON_TAXABLE - System`, plus Avalara codes) from `/public/v2/product-tax-codes`. These are categorical tax codes, not rate-based — `effective_tax_rate` and `total_tax_rate` are always `0`. Use the returned `id` and `code` to reference the tax on invoice-items (`sales_details.tax_rate.id`).

Read-only catalog: `add`, `update`, `delete` are not exposed in this release. Global system-managed codes are list-only and return 403 on `GET /tax-rates/{id}`; org-scoped codes (when present) support get-by-id.

The product tax code catalog is tenant-wide and cannot be scoped by company. The `x-apideck-company-id` header is accepted but has no effect on the returned catalog.

## GET /accounting/bills

`updated_since` is inclusive and is applied at whole-second granularity, so a poll that
reuses the newest `updated_at` it received will re-receive the records from that final
second. De-duplicate on `id`, or advance the cursor past the boundary second.

Two DualEntry behaviours combine to cause this. Its `updated_after` filter is a
`>=` comparison, and while DualEntry stores update times with sub-second precision it
returns them truncated to the second — so a returned timestamp is always slightly earlier
than the value the filter compares against. Any sub-second component supplied in
`updated_since` is therefore rounded down before the request is sent, which guarantees no
record is ever skipped at the boundary, at the cost of re-delivering that second.

## GET /accounting/bills

DualEntry scopes bills by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## GET /accounting/invoices

`updated_since` is inclusive and is applied at whole-second granularity, so a poll that
reuses the newest `updated_at` it received will re-receive the records from that final
second. De-duplicate on `id`, or advance the cursor past the boundary second.

Two DualEntry behaviours combine to cause this. Its `updated_after` filter is a
`>=` comparison, and while DualEntry stores update times with sub-second precision it
returns them truncated to the second — so a returned timestamp is always slightly earlier
than the value the filter compares against. Any sub-second component supplied in
`updated_since` is therefore rounded down before the request is sent, which guarantees no
record is ever skipped at the boundary, at the cost of re-delivering that second.

`filter[ids]` and `filter[number]` address the same field on this connector: a
DualEntry invoice's unified `id`, `display_id` and `number` are all the same value.
If both are supplied, `filter[number]` takes precedence and `filter[ids]` is ignored
— so a `number` outside the `ids` list still returns that invoice. Send one or the
other, not both.

`filter[ids]` returns at most 100 records per page. A list of more than 100 ids is not
truncated — the filter is preserved across pages, so page through the cursor to collect
them all.

## GET /accounting/invoices

DualEntry scopes invoices by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## POST /accounting/invoices

Posted invoices (`status: posted`) require `due_date`, an invoice-level memo (`customer_memo`, or `reference`
as a fallback), and a `description` on every line item. Omitting any of these returns
`400 ... Required for posted records`. A company must also be resolved — supplied by the connection's
Default Company setting, the `x-apideck-company-id` header, or `company_id` in the body (any one suffices).
Draft invoices (`status: draft`) have none of these requirements.

## GET /accounting/ledger-accounts

Ledger accounts (the chart of accounts) are tenant-wide in DualEntry and cannot be scoped by company.
The `x-apideck-company-id` header is accepted but has no effect — all accounts across the tenant are
returned regardless of the company ID.

## GET /accounting/invoice-items

Invoice items (the product catalog) are tenant-wide in DualEntry and cannot be scoped by company.
The `x-apideck-company-id` header is accepted but has no effect — all items across the tenant are
returned regardless of the company ID.

## POST /accounting/invoice-items

Creating an item requires `code` (used as the DualEntry SKU) and an income account (`income_account.id`).
Omitting `code` returns `400 ... missing sku`.

## GET /accounting/credit-notes

`updated_since` is inclusive and is applied at whole-second granularity, so a poll that
reuses the newest `updated_at` it received will re-receive the records from that final
second. De-duplicate on `id`, or advance the cursor past the boundary second.

Two DualEntry behaviours combine to cause this. Its `updated_after` filter is a
`>=` comparison, and while DualEntry stores update times with sub-second precision it
returns them truncated to the second — so a returned timestamp is always slightly earlier
than the value the filter compares against. Any sub-second component supplied in
`updated_since` is therefore rounded down before the request is sent, which guarantees no
record is ever skipped at the boundary, at the cost of re-delivering that second.

`filter[ids]` and `filter[number]` address the same field on this connector: a
DualEntry credit note's unified `id` and `number` are the same value. If both are
supplied, `filter[number]` takes precedence and `filter[ids]` is ignored — so a
`number` outside the `ids` list still returns that credit note. Send one or the
other, not both.

`filter[ids]` returns at most 100 records per page. A list of more than 100 ids is not
truncated — the filter is preserved across pages, so page through the cursor to collect
them all.

## GET /accounting/credit-notes

DualEntry scopes credit notes by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## POST /accounting/bill-credit-notes

Bill credit notes are DualEntry Vendor Credits. The unified `status` returns `draft`, `authorised` or `deleted`, mapped from DualEntry's underlying record status (`draft`/`posted`/`archived`). Only item-based lines are written on create/update — each `line_items[]` must carry `item.id`; a line without it is rejected. A credit note is allocated to a single target: supply one `allocations[]` entry of type `bill` (or `other` for a direct expense). If several are supplied a `bill` allocation takes precedence, and an `other` (direct-expense) allocation is applied only when no `bill` allocation is present; any per-allocation `amount` is ignored (the applied amount is derived by DualEntry). `update` replaces the whole record, so a partial update must resend the full credit note (`supplier`, `currency`, `date_issued`, `line_items`); a sparse update is rejected. `delete` is not supported by the DualEntry API.

## GET /accounting/customers

DualEntry customers are org-level master data and are not scoped to any company. The `x-apideck-company-id` header has no effect on customer list or lookup results — all customers belong to the organisation, not to individual subsidiary companies.

`filter[ids]` returns at most 100 records per page. A list of more than 100 ids is not
truncated — the filter is preserved across pages, so page through the cursor to collect
them all.

## GET /accounting/suppliers

DualEntry suppliers are org-level master data and are not scoped to any company. The `x-apideck-company-id` header has no effect on supplier list or lookup results — all suppliers belong to the organisation, not to individual subsidiary companies.

## GET /accounting/payments

DualEntry scopes payments by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## POST /accounting/payments

Apply a payment to an invoice via `allocations[]`: each `allocations[].id` is the target invoice's id
(its DualEntry invoice number) and `allocations[].amount` the amount applied. This is what drives the
invoice's derived `status` (`partially_paid` / `paid`). A memo (`note`) is required. Use
`status: authorised` to post the payment — `posted` is not an accepted value (allowed: `draft`,
`authorised`, `rejected`, `paid`, `voided`, `deleted`). Payments created with `status: draft` succeed
but do not affect the invoice balance or status; only posted (`authorised`) payments move the balance.

## GET /accounting/journal-entries

DualEntry scopes journal entries by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## GET /accounting/purchase-orders

DualEntry scopes purchase orders by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## GET /accounting/trial-balance

Built from DualEntry's account balances, which DualEntry documents as equivalent to a trial balance: balance sheet accounts are cumulative to `filter[end_date]` (today by default) and income and expense accounts are year-to-date in the fiscal year that contains it, with earlier years in Retained Earnings. The report covers one company: send `x-apideck-company-id` or set a default company on the connection, otherwise the request returns a `422`. Each account's signed balance, in the company's base currency, is netted into one debit or credit; accounts with a zero balance are left out. `account_id` and `code` are both the DualEntry account number, matching ledger account ids, and the line has `line_type` `account`; a balance row without an account number has `line_type` `unmapped` and a null `account_id`. `start_date` is null. `filter[accounting_method]=cash` returns a `400`. All result pages are read before responding. A report whose debits and credits differ by half a cent or more returns a `502`.
## GET /accounting/bill-payments

DualEntry scopes bill payments by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## GET /accounting/expenses

DualEntry scopes expenses by company. Pass the `x-apideck-company-id` header (or set a Default
Company on the connection) to return only records belonging to that company within the tenant.
Without it, records across all companies in the tenant are returned.

## POST /accounting/expenses

DualEntry requires `company_id`, `supplier` (vendor_id), `account` (account_number), and `currency` (currency_iso_4217_code) when creating a direct expense, even though these fields are optional in the unified schema.
## PATCH /accounting/expenses/{id}

DualEntry requires `company_id`, `supplier` (vendor_id), `account` (account_number), and `currency` (currency_iso_4217_code) when updating a direct expense, even though these fields are optional in the unified schema.

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