AutoCount Cloud Integration #
Classification: Internal-Only
AutoCount Cloud is a Malaysian cloud accounting platform. tofu authenticates with an
API key (API-Key + Key-ID headers, minted per account book in AutoCount’s
Settings → API Keys). There is no OAuth flow.
The connector reads reference data and documents, and publishes AP bills, AR invoices and direct expenses. Bank statements are not published — see Bank statements. Everything below is what the code does; where AutoCount’s own documentation disagrees, the code follows what the live API was observed to do.
Connector module: libs/rust/bonsai-integration/src/accounting/autocount/.
Support matrix #
| Type | Create | Republish | Delete / void | Attachments |
|---|---|---|---|---|
| AP bill | POST /{book}/purchaseInvoice |
delete, then create | DELETE /{book}/purchaseinvoice?docNo= — refused when knocked off |
none |
| AR invoice | POST /{book}/invoice |
delete, then create | DELETE /{book}/invoice?docNo= — refused when knocked off |
none |
| Direct expense | POST /{book}/payment (PV out, OR in) |
delete, then create | DELETE /{book}/payment?docNo= |
none |
| AP credit note | POST /{book}/purchaseReturn |
delete, then create | DELETE /{book}/purchasereturn?docNo= — refused when knocked off |
none |
| AR credit note | POST /{book}/creditNote |
delete, then create | DELETE /{book}/creditnote?docNo= — refused when knocked off |
none |
| Bank statement | not published — AutoCount has no statement API | — | — | — |
update_invoice and update_direct_expense are deliberately unimplemented.
AutoCount’s PUT is a positional patch: a shorter details array silently deletes the
trailing rows, and {} means “leave this row alone”. AutoCount is therefore in
EntityIntegration::should_delete_before_publish, and every republish is DELETE + POST.
If you ever reach one of those methods, the policy list is wrong, not the method.
Void is not wired. delete_invoice only issues a DELETE; AutoCount’s void endpoint
(which requires a voidReason) is not called, and voiding is refused on a knocked-off
document anyway.
Attachments are not supported at all #
AutoCount’s Integration API has no attachment endpoint. The web app has File Attachment; it is not exposed over the API.
The connector therefore has no attachment capability flag, and the trait methods are
inert rather than implemented: the downloads return an empty Vec,
delete_direct_expense_attachment returns Ok(()), and the uploads return
unimplemented. Do not add a capability flag to “enable” them — there is nothing behind
it.
Authentication: no scopes, no reconnect #
AutoCount has no scope list and no incremental consent, so adding publish required no new scopes and no customer reconnect. Connections made before publish shipped keep working unchanged. There is no reconnect migration for this provider and no need to write one.
Re-entering the API key for an existing integration is this provider’s reconnect. That matters for the next point.
Auto-publish is not reset on connect #
AutoCount is not in should_disable_auto_publish_on_first_connect, and the connect path
leaves the entity’s flags alone: per-extraction-type auto-publish is being reworked in
the extraction overhaul, and a provider-specific reset here would only have to be
unpicked again. An entity that already had auto-publish on keeps it.
Credit notes #
AP credit notes publish as Purchase Returns (PR), AR credit notes as Credit
Notes (CN). Despite its name a Purchase Return carries a dedicated supplierCNNo
beside supplierInvoiceNo, and with goodsReturn left off it books a supplier credit
without touching stock — which is what tofu has to publish.
Both store their amounts positive and post to the contact ledger with the opposite
sign to the invoice they reverse, so credit_note::normalized already produces the right
shape and nothing flips a sign.
A credit note publishes standalone. Applying it to an invoice is a separate Knock Off
Entry, and ourInvoiceNo on a Credit Note is free text that AutoCount neither validates
nor links. Once knocked off, AutoCount refuses both DELETE and /void, so void is
not a fallback.
Status comes from AutoCount’s own allocation labels, which are not the invoice payment
labels: Unapplied → Authorized, Partially Applied → PartiallyPaid, Fully Applied → Paid, Void → Voided.
Paid documents #
A document AutoCount considers settled cannot be removed, and a republish is a delete, so a paid document cannot be republished either.
Two sources answer two different questions, and they diverge:
- Paid is
master.status == "Full Payment"(or"Partial Payment"), which the detail read already carries for free. - Undeletable is a non-empty
GET /{book}/{purchaseinvoice|invoice}/knockoffdetails?docNo=, and nothing else. An AR invoice settled through the master’s inlinepaymentAmtreads"Full Payment"whileknockoffdetailsreturns[], and is still deletable.
delete_invoice reads the knock-offs first and refuses with
errorMessage.autoCount.DELETE_REFUSED_KNOCKED_OFF before calling AutoCount, because
AutoCount’s own refusal names the contact’s control account rather than the document
(Can't remove account "400-B001" because it was knocked off in Knock Off Entry.) and
would be unreadable in the UI. The knock-off read only costs a call on documents that
have payments.
delete_direct_expense does not pre-check. It posts the DELETE and maps AutoCount’s
refusal through map_cash_book_write_error onto the same key. The outcome matches; the
cost is one failed call.
There is no “paid, refuse edits” rule on AutoCount’s side. A PUT changing only a
description on a settled bill returns 204. The protection is entirely tofu’s.
Voided documents read back 200, not 404, with cancelled: true, status: "Void" and
all amounts retained, so map_autocount_invoice_status maps them to Voided — miss that
and a voided bill re-ingests at full value.
Quirks worth knowing before you debug #
Documents are auto-numbered. docNo is never sent. It is optional on every write
resource — AutoCount’s docs are wrong about this on Create Payment — and AutoCount answers
201 with an empty body and the number in the location header
(.../purchaseinvoice?docNo=PI-000001). Sending one invites a duplicate-number rejection
for nothing. The supplier’s own number goes in supplierInvoiceNo on AP; on AR it falls
back into ref.
The receipt voucher doc type is OR, not RV. RV is rejected outright with
400 "DocType is invalid." and matches no record in any book. The connector shipped with
RV during read-only work, which also broke reading real OR records.
Every autoFillOption flag is false, and nothing can set one. With taxCode: true,
AutoCount silently discards the line’s explicit tax code: the same P-5 on a 100.00
line stores taxCode: "P-5" / tax: 5.00 / finalTotal: 105.00 with autofill off, and
taxCode: null / tax: 0.00 / finalTotal: 100.00 with it on — and still answers 201. A
verified tax rate vanishes and the document under-posts with no error. accNo autofill is
inert in both directions and does not supply a missing account, which is a hard 400. A
test asserts all three flags.
finalTotal is the grand total; total is not. total is the sum of the line
amounts as entered — net on a tax-exclusive document, gross on an inclusive one. Mapping
tofu’s total to total under-reports every exclusive bill by exactly the tax while looking
correct on inclusive ones. netTotal equals finalTotal when there is no rounding
adjustment.
AP/AR currency follows the contact and is never sent. AutoCount ignores a currency on
the document and books in the creditor’s or debtor’s. Only currencyRate goes on the wire,
and it is always explicit: omitted on a foreign-currency contact, AutoCount applies
whatever rate the book happens to hold. tofu carries no exchange rate for AutoCount, so a
contact whose currency differs from the account book’s base currency is rejected with
errorMessage.autoCount.EXCHANGE_RATE_REQUIRED rather than published at a guessed rate.
Direct expenses do send currencyCode, and reject a non-base currency the same way.
Tax-rate ids carry an entity suffix. AutoCount exposes no tax-rate master endpoint, so
tofu seeds its own SST catalogue (uses_internal_accounting_tax_rates), and those ids are
shaped "{code}|{entity_id}" — which is what the review form stores on a line. AutoCount
knows only the bare code. autocount_tax_code (model/tax_rate.rs) strips the suffix in
both write builders and leaves a bare code alone, because the connector’s own
fetch_tax_rates returns bare codes. Without it every publish carrying a tax code
fails.
creditTerm comes from the contact, read back from
GET /{book}/creditor|debtor?code=. It is required on the master and there is no safe
constant.
purchaseLocation / salesLocation are required on AP/AR. The document’s location is
used when it has one, otherwise the account book’s default from fetch_location, otherwise
LOCATION_REQUIRED.
journalType is required on every cash book entry and has no catalog endpoint. An
unknown value is rejected (JournalType (JournalTypeKey = NOTATYPE) not exists.), and it
does not have to match the payment method’s, so it cannot be derived from the posting. The
connector takes the payment method’s own journalType, falling back to BANK or CASH
from specialAccType, because that value is guaranteed to exist in the book while a
hardcoded constant is not.
A payment method is matched by free-text name, not a code.
paymentDetails[].paymentMethod takes the name, spaces included, so the name is also the
id tofu stores. Resolving one from a GL account can be ambiguous — nothing stops two
methods pointing at the same account — so the tie-break is fixed: active first, then one
whose account really is bank or cash, then alphabetical, so an account always resolves to
the same method.
sum(paymentDetails[].paymentAmt) must equal the line total, tax-inclusive, or
AutoCount answers Total Payment not equal to Total Amount.
Only leaf departments are selectable; a parent gives
DeptNo "..." is parent department. Areas are a debtor-level dimension with no line field,
so an area_-prefixed tag id is rejected rather than dropped.
Listing conventions differ per endpoint, not per family. Document listings (invoice,
purchaseinvoice, payment) are POST and answer camelCase. Master-data listings are
mostly GET with query params (a POST returns 405), require page, and answer
PascalCase. POST /{book}/account/listing takes page as a query param and ignores the
body; POST /{book}/product/listing wants it in the body. A casing miss leaves a catalog
silently empty, which is why the payment-method model carries camelCase aliases.
Errors are one string, never a list, in {"statusCode", "message"} with no field name.
The messages are far better than the envelope suggests: they name the offending field,
value and line number, and the fiscal-year rejection even names the valid range. AutoCount
rejects over-length fields rather than truncating them.
Bank statements are not published #
AutoCount has no bank-feed API and no statement-import API — a full sitemap sweep returns zero matches. The only way to post statement data would be one Cash Book Entry per transaction, the same object a direct expense uses, which puts two different things in one drawer and risks booking the same movement twice when a receipt and a statement line both describe it.
AutoCount’s own Bank Transactions screen is the right home for this, and it is web-app only. The connector carries no bank statement publish code.
Known gaps #
- MyInvois (LHDN e-invoicing) is unprobed. Every e-invoice field is omitted, which is
the confirmed safe default (
submitEInvoice: false), and an AR request-shape test asserts an exact match so one appearing would fail. Whether a book can be configured to auto-submit, and whether that blocks delete, needs a throwaway account book — never a real one, because deleting a document does not undo a tax filing. autocount_compatibility(model/account.rs) is too permissive. It marks cash and debtor/creditor control accounts valid on AP/AR lines; AutoCount rejects them (AccNo "310-1000" is not valid account for posting.). They are legal on a cash-book line, so the rule is line-type-specific. The tofu validator catches it; the read-side flag still lies.- The shared
is_publish_accountexcludesBank, so a bank account on a cash-book line reportsACCOUNT_NOT_FOUNDalthough AutoCount accepts it. Pre-existing and shared across providers, so it is pinned by a test rather than changed. - Several error keys have no caller.
ATTACHMENTS_NOT_SUPPORTED,VOID_REASON_REQUIRED,CREDIT_TERM_UNKNOWN,TAX_DATE_BEFORE_DOC_DATE,CURRENCY_NOT_IN_BOOK,ITEM_VARIANT_REQUIRED,TAX_CODE_UNKNOWNandDOC_DATE_OUTSIDE_FISCAL_YEARare in the catalog and translated, but nothing returns them: the parity test only checks that the Rust list anderror.jsonagree. The tofu validator catches most of these first under its owninvoiceValidation/directExpenseValidationkeys, and anything it misses surfaces as AutoCount’s own message. Do not assume a key here means a mapped path. - Deep-link URLs to AutoCount’s web UI are unverified against the live product.
- Line descriptions are not length-capped. AutoCount caps a detail
descriptionat 100 and rejects rather than truncating, so a long line fails the publish with its raw message. The masterdescriptionis capped at 80 and is truncated, with the full text kept innote. - Eight combination probes remain open — foreign-currency AR, product variants, inactive master data, 5-cent rounding, an approval workflow, a closed period, and a scoped API key for 403-versus-401 — each needing an account book the test book cannot provide.
Related documentation #
- Integrations overview
- Bukku — the other API-key Malaysian connector
- Feature Flags