blog·5 min read

Eight write actions and no ninth: how our software writes safely to an accounting package

An integration that reads cannot break anything. One that writes can. How we limit writing to Exact Online and Nmbrs: a fixed list, tests that guard the list, look first and then book.

Put this invoice into the accounting as a purchase order, but show me
exactly what you are going to book first.

An integration that only reads from an accounting package cannot break anything. At worst it is slow or out of date. An integration that writes can make a hundred wrong bookings in one night, and you have to remove those by hand.

In recent weeks we built two integrations that both write: one for a drinks wholesaler with a shop that works with Exact Online, and one for a café that works with Nmbrs Accounting (formerly Yuki). The first has been running since October, the second is in testing. They share one way of working for everything that goes into the accounting. It is described below. The clients stay anonymous and there are no amounts, prices or margins here.

Writing is a list, not a right

Reading and writing live in two separate pieces of code. The reading part may ask for anything it needs. The writing part knows a fixed list of actions, and nothing else.

At the wholesaler there are eight, from a stock transfer out of the till export to setting a conversion factor on a supplier item. At the café there are two: have a sales invoice processed and put a document in the purchase folder.

A new write action is only added when the owner explicitly asks for it, and then as a job of its own with its own delivery. Not as a side effect of something else. That sounds bureaucratic, but it is exactly the question you want someone to ask: should this software really be allowed to do this?

A test that guards the list

Agreements in a document get forgotten. That is why the list is also in a test.

That test looks at the code itself. Does the writing class only have the agreed functions? Does it only send to the fixed addresses in the API? Does it only use the methods that belong there, so no delete command? And is it only called from the places we agreed? A second test searches the whole codebase for network traffic outside the allowed classes.

If a write action appears anywhere that is not on the list, the build fails and nothing goes live. That applies to us, and just as much to an AI assistant that helps build. It can write code quickly and well, but it cannot move this boundary unnoticed. The project instructions also say that these tests may not be relaxed.

A check before anything leaves the building

Every request to the accounting first passes a check in our own code. Only known fields, valid references, a quantity that is not zero, a price within reasonable limits, no more than a fixed number of lines.

The clever part is where else that check runs: when the proposal is shown. If a booking would fail the check later, you already see it on screen, as a block with an explanation, before anyone presses the button. So you do not get an error from the accounting package afterwards, but a red line beforehand that says what is missing.

Look first, then book

Every write action starts with a proposal. You see which lines go in, with which quantities and amounts. Below it is a tick box: "I have checked the proposal". Without the tick, no button.

The form also sends along the number of lines and the total that you saw on screen. If the proposal has changed in the meantime, for example because an invoice was read again, the booking is refused. So you always book what you have seen.

Two things go with this. Commands that write from the command line run as a dry run by default and only do something for real with an extra flag. And after every write action we read back what is now actually in the accounting. Why that is needed, we wrote in what the Exact documentation does not say.

Uncertain is a state of its own

The hardest moment for a writing integration is not the error. It is the timeout. You sent something and no answer comes. Is it there now or not?

At the café that is called "uncertain", and it is a state of its own next to succeeded and failed. The app then does not retry by itself, because that risks a duplicate invoice. An administrator looks in Nmbrs and chooses: it is there, or try again. If logging in fails, nothing was sent and it is simply an error. If sending fails, no invoice number is used up either.

At the wholesaler, purchase orders work differently, and that is not a contradiction. There we do retry on a timeout or a busy API, at most three times, after two, ten and thirty minutes. But every attempt starts with a check in Exact whether the order is already there, on the invoice number in the reference. Retrying automatically is only allowed if you can reliably check whether the first attempt arrived. If you cannot, a person decides.

Developing without a connection

The development environment never talks to a real accounting package. With Exact there is also a technical reason: whoever refreshes the access token invalidates the other one. A test environment that joins in brings production to a halt.

For the café, the development environment runs against a rebuilt Nmbrs server. With a few code words in a description you make it reject an invoice or let the connection hang, so you can test the error paths without waiting for a real outage. The very first real write action went through a yes/no question on the phone, as in the yes/no button for your AI agent.

Common mistakes

"Give the integration write rights now, it may be useful later." Then the integration decides what it may do, instead of the owner. Start with reading and add write actions one by one.

"The package returned a 201, so it is in there." A 201 says your request was accepted. What is in there, you only know once you have read it back.

"On a timeout we just try again." Only if you can first check whether the first attempt arrived. Otherwise you book twice.

"The rules are in the documentation." Rules that are not in a test are intentions. Fix the list of write actions in code that fails when it is exceeded.

"The test environment uses the same connection, just with test data." For packages that rotate the access token, that knocks production out. Run development work against a copy or a rebuilt server.

This is how we build every integration, whether it is Exact Online, MplusKASSA or Nmbrs. If you want something similar for your own administration, take 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