blog·5 min read

The Nmbrs Accounting API: what the manual does not say

What we ran into while building an integration on Nmbrs Accounting, the former Yuki: an XML field that may not be text, a date filter that filters on something else, and fields with three shapes.

Build an integration on our accounting that reads out the cash book, the
outstanding invoices and the purchase archive.

In October we built an integration on Nmbrs Accounting, the package that was called Yuki until September. Reading the ledger, the outstanding items and the purchase archive, plus two write actions. Built and tested against the real API, for a cafe. The client stays anonymous and there are no amounts, prices or margins here.

The documentation describes the operations neatly. What is in it is correct. What is not in it cost us the most time. This is that list.

We already wrote about the rename and the basics, such as the address, the session and the daily limit, in Yuki is now Nmbrs Accounting. That is also where the most important rule is: a misspelled parameter name is silently ignored, and the spelling differs per service. The sales service uses sessionId and administrationId, the read services sessionID and administrationID.

An XML field that may not be text

Offering sales invoices goes through an operation with a field called xmlDoc. That field looks like a place for a string. It is not. The document with the invoices has to go in as a real XML element, as a child of xmlDoc, not as text with the angle brackets encoded away.

On top of that, the document needs two namespaces: its own namespace for sales invoices and the XML Schema Instance one. Leave out either of them and Nmbrs refuses the whole document. The error message does not say which one is missing.

And the order of the elements counts. There is an official schema file for it; we validate our document against that in a test before it goes out. That catches a misplaced element on your own machine instead of in a client's accounting.

The date filter filters on something else

The purchase archive has an operation that lists the documents in a folder, with a start and end date. You expect those dates to be about the invoice date. They are not: they filter on the moment the document came in.

For accounting that is a real difference. An invoice from late September that gets mailed in October falls outside a filter on September. So if you want to close a month, you have to fetch wider than the month itself and then select on the document date.

Paging works here with a start record and a count. We fetch 200 at a time and stop when a page returns fewer than 200. You do not get a total.

Fields with three shapes

The answers are XML, and the library we use turns them into arrays. That means one field can have several shapes:

An element that occurs once is an object. More often, and it is a list. A parser that counts on a list breaks on its first contact with exactly one invoice. We run everything through one function that always makes it a list.

A contact ID can come back as a list of two identical values. Sometimes as plain text, sometimes as the same value twice. Normalise to the first non-empty value.

A type is sometimes an object with a code and a text. Not just the text.

Names can be double-encoded. A company name with an ampersand comes back with the encoding in it twice. Decode it, or the name ends up wrong in your own screen.

And a nice one: in the ledger scheme a field is called descripton. With a typo, in the API itself. Write your parser against what the API sends, not against what it should say.

One call that gives almost everything

The operation for transaction details asks for a ledger account. Leave it empty and you get all accounts in one answer. Half a year is then a few megabytes in a single call.

That is worth gold if you are doing an analysis, because the daily limit is 1,000 calls per domain and logging in counts. One call instead of a loop over 400 accounts makes the difference between fitting and not fitting. But watch your memory and your timeout: it is a big document.

What you cannot get out of an entry

Two things we ran into while building overviews:

The user who mutated something is always the package itself. Even for manual work. So you cannot see who booked what.

There is no entry date. The transaction date is the booking date: the invoice date, the bank date or the week date. When somebody typed it in is not in there.

And one thing that does work: the line ID is per line, not per entry. An entry with five lines is grouped on the document reference. For purchase invoices that is the supplier's invoice number, which is handy if you want to check whether an invoice is already in the accounting.

Refusal against uncertainty

For writing we put one distinction firmly in the code, and we would do it that way again anywhere.

An error from the service itself is a refusal: nothing is booked, you can safely try again. A timeout, a broken connection, a server error without a proper error message or an answer you cannot read is something else. Then you do not know whether the invoice is in there.

Our integration never automatically retries that second kind. There is a message saying you should first look in the accounting to see whether the invoice is already there. A duplicate sales invoice is far more annoying than an invoice that goes out a day later.

Common mistakes

Filling xmlDoc with text. It has to be a real XML element, with both namespaces, in the order of the schema.

Using the archive date filter for invoice dates. It filters on arrival. Fetch wider and select yourself afterwards.

Doing one call per account. One call without an account code gives everything. With 1,000 calls a day that is not a detail.

Starting every question with a new session. Logging in counts towards the limit. Request one session per service and keep it.

Automatically resending after a timeout. A timeout is not a refusal. Let a human look before you push a second invoice into the accounting.

What an integration on this package can do is on connecting Nmbrs. If you want something like this for your own administration, have a look at custom software.

MK
Maarten Keizer

Founder of Invoker. Over twenty years in hosting, system administration and web development; builds the Claude hosting himself and tests everything on our own servers first.

about maarten