# Exact Online NL — Accounting API: gotchas

> Canonical URL: https://developers.apideck.com/apis/accounting/exact-online-nl/gotchas

## GET /accounting/tax-rates

`status` is derived from Exact's `IsBlocked` flag (`false` -> `active`, `true` -> `inactive`). Live-verified against a real division: the raw VATCode object has no `Active`-equivalent field of its own, only `IsBlocked`. Exact has no third state, so `archived` is never returned.

## GET /accounting/bills

Only `updated_since`, `bill_number` and `billed_since` can be filtered on. Exact Online
refuses a filter on the fields behind `reference`, `due_since` and `status` — its
purchase-invoice endpoint answers *"Filtering on these fields is not supported"* — so
those are rejected rather than silently ignored.

A filter value Exact cannot be asked about, such as a malformed date or a non-numeric
`bill_number`, returns an **empty page rather than an error**, so an empty result does
not by itself mean there are no matching bills.

## POST /accounting/bills

`line_items[].ledger_account` is not supported on create — Exact's purchase-invoice line
object rejects an unknown `GLAccount` property outright (400, "The property name
'GLAccount' ... is not valid"), so the connector does not send it: a `ledger_account` on
a create request is silently dropped rather than rejected, and the created bill line will
not be coded to that ledger account. Code the expense via `line_items[].item` instead, or
use `journal-entries` for a GL-coded posting.

## GET /accounting/invoices

Exact Online has no balance field on an invoice, so `balance` is derived from its
cash-flow data. When that derivation cannot run — the connection's app registration
lacks cash-flow access, or the call is rate-limited or fails — `balance` comes back
`null`, meaning *not determined*, never `0` and never the invoice total. Treat `null`
as unknown rather than unpaid.

A draft invoice is the exception: it has no number, nothing is derived for it, and its
`balance` is its own total, which is correct since none of it has been settled.

Reading a single invoice by id derives the balance the same way, so the two paths agree.

Exact Online's own invoice lifecycle carries four states, mapped to `draft` (not yet
issued, no number), `submitted` (issued, not yet due), `unpaid` (past its due date and not
settled) and `paid`. It has no part-paid state of its own, so `partially_paid` is derived
here: an invoice whose `balance` is above zero but below its `total` is reported
`partially_paid` rather than `unpaid` or `submitted`.

That derivation needs the balance, so it is skipped whenever `balance` is `null` — an
invoice whose balance could not be determined keeps the state Exact gave it.

An invoice that carries no number is reported `draft`, which is also how `balance` treats
it. Should Exact ever return an invoice with a number but no recognisable lifecycle value,
`status` is `null` — *not determined* — rather than guessed at.

**Sales credit notes are returned here too.** Exact stores them in the same entity as
invoices, so this endpoint serves both, and they are marked `type: "credit"` (an ordinary
invoice is `type: "standard"`). Three things follow. Their `balance` is what the customer
is owed, not what they owe, and Exact does not enforce a negative sign on the amounts — a
credit note can carry a positive `total` and a positive `balance`. The same document is
also served by `/accounting/credit-notes` under a **different** id, so syncing both
endpoints double-counts it unless `type` is used to filter here. And `status` on a credit
note is Exact's own document lifecycle, not a refund state.

A filter value Exact cannot be asked about — a malformed date, an `ids` or `customer_id`
that is not a GUID, a non-numeric `number` — returns an **empty page rather than an
error**, so an empty result does not by itself mean there are no matching invoices.

## POST /accounting/invoices

Exact Online assigns invoice status automatically (`open` by default) and ignores the `status` field on create — invoices are finalized/processed in Exact Online, not via the API.
Set VAT via `line_items[].tax_rate.code`; Exact derives the percentage from the code, so `line_items[].tax_rate.rate` is ignored on create.
`shipping_address` creates a CRM address record against the customer, but **does not
attach it to the invoice**: the invoice is saved with whatever delivery address the
customer already had, so only the first address ever sent for a given customer takes
effect. A later, different address is stored on the customer and then ignored by the
invoice. Set the delivery address on the customer record when it needs to change.

On read, `shipping_address.string` returns that address record's identifier rather than a
formatted address, and the individual lines are not returned.

## GET /accounting/invoices/{id}

Exact Online has no balance field on an invoice, so `balance` is derived from its
cash-flow data. When that derivation cannot run — the connection's app registration
lacks cash-flow access, or the call is rate-limited or fails — `balance` comes back
`null`, meaning *not determined*, never `0` and never the invoice total. Treat `null`
as unknown rather than unpaid.

A draft invoice is the exception: it has no number, nothing is derived for it, and its
`balance` is its own total, which is correct since none of it has been settled.

The list endpoint derives the balance the same way, so the two paths agree.

Exact Online's own invoice lifecycle carries four states, mapped to `draft` (not yet
issued, no number), `submitted` (issued, not yet due), `unpaid` (past its due date and not
settled) and `paid`. It has no part-paid state of its own, so `partially_paid` is derived
here: an invoice whose `balance` is above zero but below its `total` is reported
`partially_paid` rather than `unpaid` or `submitted`.

That derivation needs the balance, so it is skipped whenever `balance` is `null` — an
invoice whose balance could not be determined keeps the state Exact gave it.

An invoice that carries no number is reported `draft`, which is also how `balance` treats
it. Should Exact ever return an invoice with a number but no recognisable lifecycle value,
`status` is `null` — *not determined* — rather than guessed at.

**Sales credit notes are returned here too.** Exact stores them in the same entity as
invoices, so this endpoint serves both, and they are marked `type: "credit"` (an ordinary
invoice is `type: "standard"`). Three things follow. Their `balance` is what the customer
is owed, not what they owe, and Exact does not enforce a negative sign on the amounts — a
credit note can carry a positive `total` and a positive `balance`. The same document is
also served by `/accounting/credit-notes` under a **different** id, so syncing both
endpoints double-counts it unless `type` is used to filter here. And `status` on a credit
note is Exact's own document lifecycle, not a refund state.

## GET /accounting/bill-credit-notes

Exact Online does not track settlement on a purchase entry, so `balance`, `remaining_credit`, `date_paid` and `allocations` are never returned. `tax_inclusive`, `account`, `tax_code`, `subsidiary`, `location`, `department`, `tracking_categories`, `custom_fields` and `row_version` are likewise not populated.
`status` reflects Exact's entry status: an open entry reads as `authorised`, a processed one as `paid`, and a draft as `draft`. Any other status Exact may return falls back to `authorised`.

## POST /accounting/bill-credit-notes

Requires a purchase journal code on the connection setting `purchase_journal_code`; without it Exact rejects the create. Vault presents that setting under the Bills resource, and the same value applies here. Line item `total_amount` must be a positive amount of at least one cent — Exact derives the document type from the amount sign and rounds to two decimals, so a zero, negative or sub-cent line would record a bill instead of a credit note and is rejected.
Exact refuses a duplicate: a credit note whose supplier, `reference`, `date_issued` and total all match an existing one is rejected with `Already exists`, reported as a `500` rather than a conflict. Retrying a create that already succeeded therefore fails instead of producing a second record — vary `reference` when a repeat is genuinely intended.
`line_items[].total_amount` is always booked as the amount excluding tax and `tax_inclusive` is ignored, so tax from the line's `tax_rate.code` is added on top: a line of `100` with a 21% code produces a credit note whose `total_amount` is `121`. This also applies to a tax code Exact presents as tax-inclusive, so sending a gross amount on such a code overstates the credit note by the tax.

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

Only header fields are updated, and only these: `currency`, `currency_rate`, `note`, `date_issued`, `reference` and `terms`. Anything else sent on an update is accepted with a `200` and discarded — including `supplier`, `status`, `number` and `total_amount`.
`line_items` are likewise ignored, so amounts, descriptions and accounts keep their existing values. That is a limitation of this connector rather than of Exact Online, which does allow a booked line to be edited; changing a line through Unify currently means deleting the credit note and creating it again.

## POST /accounting/suppliers

Exact does not enforce uniqueness on the supplier name — posting the same `display_name`/
`company_name` twice creates two separate suppliers, both with a `201`. This connector
passes the name through as sent rather than matching on write; the caller owns
de-duplication (for example, checking `suppliers.list` for an existing name, or storing
the returned `id` and reusing it) before creating a supplier that may already exist.

## GET /accounting/payments

Payments are read from Exact Online's cash-flow area, so the Exact Online app registration
needs read access to it (the `financial.cashflow` scope, set in the app's Data & Security
Review). Without it, every call returns a 403 `AppScopeViolated` error while the connection
still shows as Connected and other resources keep working. Invoice `balance` is derived from
the same area and comes back `null` in that case.

## POST /accounting/payments

Creating a payment records the bank transaction but does not automatically reconcile it
with the linked invoice — the invoice status will not change to paid until the payment is
matched manually in Exact Online. A valid bank journal (Type=12) must be supplied via the
`bank_journal_code` connection setting or `pass_through[journal_code]` per request;
without one the API returns 'Invalid journal'.

## GET /accounting/journal-entries

Limit parameter for pagination is not supported for this resource.

The list returns the same `line_items` as the single-record read, so no fetch by id is
needed for line-level data.

Exact's journal entry header has no date field, so `posted_at` is derived from the lines:
it is the earliest `line_items[].date`. Only an entry with no dated line falls back to the
first day of its financial period (`FinancialYear`/`FinancialPeriod`), which is month
precision. `created_at` is when the entry was created in Exact, not its accounting date.

`title` is the journal's own description. `memo` is the first non-empty
`line_items[].description`, or null when no line has one. `updated_by`/`created_by` are
not populated: Exact's header records when it was created and modified, but not by whom.

Exact does not distinguish a customer account from a supplier account at the line level,
so each line's account is returned under both `line_items[].supplier` and
`line_items[].customer`. To tell which it is, match the id against the suppliers or
customers list; an account that is both appears in both.

## POST /accounting/journal-entries

A validation refusal — an unbalanced entry, an unknown or inactive reference (ledger
account, journal, VAT code, currency), or a missing required field — returns `422` with
the specific reason named. Any other `500` is a genuine unexpected downstream failure,
not a refusal the request itself caused.

VAT on a line is selected by `tax_rate.code`, not `tax_rate.rate` — the rate is not sent
and has no effect. Omitting the code does not mean "no VAT": Exact substitutes the ledger
account's own default VAT code, which changes the line's tax amount and can then reject
the whole entry as unbalanced with an error naming the balance mismatch rather than VAT.
Always send `tax_rate.code` explicitly rather than relying on a specific ledger account's
default.

`line_items[].date` maps to Exact's per-line `Date` (a real field, distinct from the
header, which has no date field at all — see the list-operation gotcha above). Historical
or backdated bookings should set this per line rather than relying on `posted_at`.
`line_items[].source_id` maps to Exact's per-line `Notes`, useful for carrying your own
reconciliation identifier since `description` is typically a human-readable label.

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

`reference_id` is the invoice, bill, or journal entry's own unified id — not Exact's
internal Document envelope GUID. The connector resolves between the two internally
(Exact's `FinancialTransactionEntryID`, live-verified to equal the unified id for all
three reference types), so no manual lookup or pass-through is needed.

`file_url` is Exact's own document viewer page
(`https://start.exactonline.{tld}/docs/SysAttachment.aspx?...`), not a direct file
download link — it requires an active, logged-in Exact browser session and redirects to
a login page for an unauthenticated request (live-verified: a plain GET returns a 302 to
the Exact login page, not the file). There is no other download-capable URL on this
connector's attachment records.

This list has no server-side total count, so pagination metadata never includes a count.

## GET /accounting/journals

Use code as journal_symbol when creating a journal entry. allow_vat applies to general journals. x-apideck-company-id selects the division. This list has no server-side total count, so pagination metadata never includes a count.

## POST /accounting/journals

`type: bank` requires `iban` — Exact needs a bank account reference to create a bank journal. `type: payment_service` requires a payment service provider reference that has no equivalent in this unified field set — creating a journal with that type will be rejected by Exact. Use `pass_through` to supply the required vendor field if you need `payment_service`, or use `sales`, `sales_credit_note`, `purchase`, `purchase_credit_note`, `cash` or `general` instead.
`type: other` has no Exact equivalent and will be rejected — `other` is a read-side fallback for values Exact might introduce in the future, not a valid input.
`clearing_account` is the unallocated account a bank journal parks incoming movements in. Bank feed statements posted to a journal without one never reach the reconciliation queue. Exact assigns a default on create; set it explicitly to pin a specific account. Only meaningful on `type: bank` and `type: payment_service`.

## GET /accounting/fiscal-periods

Periods of the selected division, in fiscal year and period order. `name` is generated as `{fiscal_year}-P{period_number}` (for example `2026-P01`) because Exact has no period name. `fiscal_year` is the calendar year in which the period's fiscal year ends, taken from the latest period end of that fiscal year (one extra read of the division's periods; null if it cannot be read); Exact's own financial year label, which can be the start year, is the first part of `name`. `status`, `locked_through` and `is_adjustment` are not exposed by Exact and are always null.

## GET /accounting/trial-balance

Built from `financial/ReportingBalance`, which is kept per fiscal period. `filter[end_date]` is resolved to the fiscal period that contains it, and the report is as of the end of that period (`end_date` in the response); `start_date` is the first day of that fiscal year. Income and expense accounts cover that fiscal year up to and including that period. For the current and previous fiscal year, balance sheet accounts are Exact's opening balance for that year (after the result appropriation, the same source as the balance sheet) plus that year's movements up to and including the period; for older years they are summed over every earlier fiscal year, which reads the ledger's full history and can take many requests or fail with a `502` on a long-lived administration. Accounts with a zero balance are left out. A result of earlier years that has not been appropriated to equity is returned as one extra line, "Result of previous fiscal years (not yet appropriated)", with `line_type` `unappropriated_result` and null `account_id` and `code`. On the opening-balance path that line is the opening balance's own net, negated, never derived from the requested year's movements, so a report whose debits and credits do not balance returns a `502` instead of being absorbed into it; an empty opening balance is not trusted and the earlier years are read instead. `account_id` is the G/L account's id, the same id the ledger accounts take; a balance without an account would be `unmapped`. Exact is accrual-only: `filter[accounting_method]=cash` returns a `400`. Without `filter[end_date]` the current period is used, today in UTC (the latest period if today is past the last one); a date outside every fiscal period returns a `400`, and a division without any financial period returns a `422`. `currency` is null.
## GET /accounting/bill-payments

Bill payments are read from Exact Online's cash-flow area, so the Exact Online app registration
needs read access to it (the `financial.cashflow` scope, set in the app's Data & Security
Review). Without it, every call returns a 403 `AppScopeViolated` error while the connection
still shows as Connected and other resources keep working. Invoice `balance` is derived from
the same area and comes back `null` in that case.

## POST /accounting/bill-payments

Creating a bill payment records the bank transaction but does not automatically reconcile
it with the linked bill — the bill's status will not change to paid until the payment is
matched manually in Exact Online. A valid bank journal (Type=12) must be supplied via the
`bank_journal_code` connection setting or `pass_through[journal_code]` per request;
without one the API returns 'Invalid journal'.

## POST /accounting/bank-feed-accounts

Not supported. A bank feed account is a bank journal in Exact Online, and a usable one needs
a linked bank account and an unallocated account this operation cannot supply. Create it via
the `journals` resource — which exposes `iban`, `default_account` and `clearing_account`
— and pass its `id` as `bank_feed_account_id`. Reading is unaffected;
delete is also not wired.

## GET /accounting/bank-feed-statements

Use `filter[bank_feed_account_id]` to read one account's statements; it accepts the account
id or its short code as shown in Exact Online. An unfiltered read spans every bank account
in the division.

## POST /accounting/bank-feed-statements

Every line is posted to the unallocated account configured on the target bank feed account,
which is what puts it in the bookkeeper's reconciliation queue. An account with no
unallocated account configured is rejected with a `400` — set one via the `journals`
resource's `clearing_account`.

`status` always reads as `success`; Exact has no delivery state. `start_balance`/`end_balance`
are ignored on write, since Exact computes them. `counterparty` and `merchant_category_code`
are not persisted. Reposting a statement creates a duplicate — Exact does not deduplicate on
`source_transaction_id`.

`source_transaction_id` is kept in the line's free-text notes field, which the bookkeeper can
edit in Exact Online. A line edited there reads back with whatever they typed, so treat the
value as advisory when matching against your own records.


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