Changelog 2026-09-07
Your bank accounts are now in the API, metadata stops overwriting itself, and payloads we had started refusing are welcome again.
Banking, opened up
Section titled “Banking, opened up”Bank accounts and their transactions are now part of the API.
Accounts:
GET /v1/BankAccountsandGET /v1/BankAccounts/{id}POST /v1/BankAccountsto create one,PUT /v1/BankAccounts/{id}to update itDELETE /v1/BankAccounts/{id}POST /v1/BankAccounts/SetAsMain/{id}— moves the main-account designation from whichever account holds it, since exactly one account is main at a time
Transactions, read-only:
GET /v1/BankAccounts/{id}/Transactions— paged, withfrom,to,searchTerm,hideExcludedandhideReconciliatedGET /v1/BankTransactions/{id}POST /v1/BankTransactions/Exclude/{id}andPOST /v1/BankTransactions/Include/{id}— set a transaction aside so it stops being offered for reconciliation, or bring it back
Connected accounts are read-only
Section titled “Connected accounts are read-only”An account synchronized from a provider mirrors what that provider reports, so Altoviz is not its system of record and neither are you.
Every account carries a provider object — type, accountId, accountStatus, bankName — which is null on an account you keep by hand, and a connected flag that says the same thing. One null check tells you whether an account is yours to write to.
PUTandDELETEon a connected account return400. Disconnect it in Altoviz first.- The provider fields are never writable, on any account. They are absent from the create and update bodies entirely.
Setting the main account and excluding or including a transaction still work on a connected account: those are Altoviz’s own bookkeeping, not something the provider owns.
Deleting is deliberately hard to do by accident. Beyond the connected check, DELETE returns 409 when the account holds any transaction already reconciled with a receipt or a payment, and 400 when the account is the main one. What gets through is an account created by mistake or never used — which is the only kind you should be deleting through an API, because deletion also removes the account’s accounting journal and every entry in it.
Metadata that stops fighting you
Section titled “Metadata that stops fighting you”metadata is now available on customers and products. It was already stored and already used — the WooCommerce import writes woocommerce_product_id, Quickbooks writes qbo_id — you just could not read it back.
More importantly, metadata is now a patch everywhere the API carries one:
- keys you send are added or updated
- keys you leave out keep whatever they held
- a key is removed only when you send it as
null ""stores an empty value, it does not remove anything- omitting the property entirely leaves the whole bag untouched
The bag is shared. Your integration is not the only thing writing to it, and replacing it wholesale meant sending your own single key silently dropped everyone else’s.
⚠️ Breaking change: if you relied on omitting a key to remove it, send that key as null instead. This affects invoices, quotes, credits, receipts, colleagues, MarkAsPaid and MarkAsRefunded, which previously replaced the whole bag.
Finding documents you didn’t create
Section titled “Finding documents you didn’t create”Find accepted an internal id and nothing else — the one key a document created inside Altoviz never has. So documents made in the app were invisible to it.
GET /v1/SalesQuotes/Find,/v1/SalesInvoices/Findand/v1/SalesCredits/Findnow take anumberalongsideinternalId, matching customers and products. When both are given,internalIdwins.
A draft carries a provisional number from its own sequence (000001) until finalization assigns the real one (FA001000), so a search by number can match either.
GetByInternalId also spread beyond the three resources that had it:
GET /v1/Products/GetByInternalId/{internalId}GET /v1/Receipts/GetByInternalId/{internalId}GET /v1/SalesQuotes/GetByInternalId/{internalId}, and the same forSalesInvoicesandSalesCredits
Sales, with an s
Section titled “Sales, with an s”Sale documents were the only place in the API spelled without one. /v1/Exports/SalesInvoices always had it; /v1/SaleInvoices did not.
The canonical routes are now /v1/SalesInvoices, /v1/SalesQuotes and /v1/SalesCredits.
Nothing breaks. Every /v1/Sale* route is still served, returns exactly the same result, and is simply flagged deprecated in the specification, naming the route that replaces it. Move when it suits you.
We stopped being picky
Section titled “We stopped being picky”Some payloads that had worked for years started coming back 400. Loosely-typed clients send what they send, and we had quietly narrowed what we accept. That is fixed, and now guarded by a test that re-types every field of every published model and asserts we still accept whatever we used to.
Accepted again:
- a number where a string is declared —
"woocommerce_order_id": 3587755 "true","1","0"or""where a boolean is expected""where an optional number, date or enum is expected
⚠️ One exception, on purpose: a quoted number carrying a group separator, like "1,5", is now refused with a 400 naming the field. It used to be read as 15 — a silent factor of ten, on money. "1.5", "1000" and 1000 are unaffected.