← Blog

Integrations

Exact, AFAS, Twinfield: 16 quirks die je grootboek slopen

Zestien quirks uit Exact Online, AFAS Profit en Twinfield REST, geleerd in vier maanden boekhoud-agent bouwen, gerangschikt op wie stilletjes je proefbalans sloopt.

Jacob Molkenboer· Oprichter · A Brand New Company· 21 apr 2026· 9 min
Open leren grootboek op ivoorpapier met messing clips, groen tabje, messing kroontjespen en rode lakzegel in zijlicht.

Maandagochtend in Zwolle. De senior controller bij een MKB-accountantskantoor van 25 man punt de batch van vrijdag af. De proefbalans staat €0,03 scheef. Dan €0,07. Tegen koffietijd is het gat €1,41 verdeeld over 47 boekingen. De boekhoud-agent draaide 's nachts schoon door — groene vinkjes, elke regel bevestigd met een 200 OK. Het geld is alleen… afgerond.

Afgelopen winter hebben we vier maanden besteed aan die agent voor een kantoor met twaalf klanten. Hij leest inkoopfacturen uit een gedeelde inbox, classificeert ze, boekt ze in het boekhoudpakket van iedere klant, en jaagt via Teams achter de akkoorden aan. Twee klanten draaien op Exact Online, acht op AFAS Profit, twee op Twinfield. Onderweg liepen we tegen zestien REST-quirks aan. De goedkope gooien een 400 terug en die fix je op dag één. Vier andere geven netjes een 200 OK terug en slopen stilletjes je proefbalans. Hieronder de ranglijst — stille moordenaars bovenaan, dag-één-fixes onderaan.

Tier 1: stille datacorruptie

Dit zijn de vier die netjes 200 OK teruggaven en stiekem je boeken uit het lood trokken. We hadden ze pas door in week drie, nadat de controller bij een aangifte-preview aan de bel trok.

1. Exact Online — BTW-verlegd op €0,05 afgerond bij deelcredits

Stuur je een POST met een creditnota tegen een BTW-verlegde verkoopfactuur, dan rekent Exact de impliciete BTW-regel server-side opnieuw uit en rondt af op €0,05. De response toont je ingestuurde bedragen. De boeking die naar SalesEntryLines wordt weggeschreven toont de afgeronde waarde. Originele factuur €1.247,83 met 21% verlegd, credit van €312,41, verwachte BTW €65,61, werkelijke €65,60. Drie cent die niemand opmerkt, totdat de kwartaalaangifte-preview over veertig klanten scheef staat.

Workaround: boek de credit als handmatige journaalpost op GeneralJournalEntries, niet tegen de oorspronkelijke factuur. Je verliest de koppeling, maar je houdt de centen.

2. Twinfield — tussenrekening boven €5.000 verliest de grootboekrekening-mapping

Journaalposten boven €5.000 die op een tussenrekening (kruisposten, vraagposten, kas-in-transit) landen, verliezen de dim1-mapping als je de viercijferige code stuurt in plaats van de variant met office-prefix. 200 OK. De boeking is zichtbaar in de UI. Het grootboekrekening-veld is null. Je proefbalans klopt nog, omdat de tegenboeking zijn mapping wél meeneemt — maar in de kolommenbalans-export valt de regel in de bak 'niet toegewezen'.

<!-- Silently strips mapping above €5,000 -->
<line type="detail">
  <dim1>2510</dim1>
  <value>6712.40</value>
</line>

<!-- Works -->
<line type="detail">
  <dim1>KRUIS-2510</dim1>
  <value>6712.40</value>
</line>

3. AFAS UpdateConnector — hoofdlettergevoelige veld-ID's, stil genegeerd

DaRe is 'Datum reservering'. DARE is niets. De connector geeft 200 terug, elk veld met de juiste casing wordt bewaard, en het verkeerd gespelde veld belandt in de prullenbak. Oudere codebases die naar boven- of onderkast normaliseren, zetten zo de helft van een record stilletjes op NULL. AFAS documenteert de canonieke casing per veld, maar geeft geen validatiefout terug als het niet klopt.

4. Exact Online — lege CostCenter werkt door naar gekoppelde journaalregels

POST een TransactionLine met "CostCenter": null op een journaalpost die al aan een tweede regel is gekoppeld, en Exact wist de CostCenter op álle gekoppelde regels. In een forumdraad uit 2019 omschreven als 'verwacht gedrag', niet terug te vinden in de officiële referentie. Stuur dus altijd de vorige waarde ongewijzigd terug, tenzij je 'm echt wilt leegmaken.

Let op

Quirks 1, 2 en 3 vervormen je proefbalans zonder ook maar één error-log-regel achter te laten. De enige betrouwbare verdediging is een verificatielaag die iedere geboekte post terugleest en met een checksum vergelijkt met wat je verstuurde.

Tier 2: 200 OK maar stilletjes mis

5. Exact OAuth-refresh — 30 seconden klok-drift en je ligt eruit in productie

Refresh-tokens zijn op het auth-endpoint geldig tot 30 seconden in de toekomst. Loopt je auth-server meer dan 30 seconden achter op NTP, dan krijg je invalid_grant. Loopt hij vóór, dan komt het access-token terug met nul seconden levensduur. 'Op mijn laptop werkt het wel' in zijn puurste vorm. De REST-referentie van Exact noemt het venster in één zin op de OAuth-pagina.

6. Twinfield — SessionID verloopt midden in een batch, response-envelope blijft 200

Sessies verlopen na 20 minuten zonder activiteit. Lange ProcessXML-batches lopen daar af en toe tegenaan. De HTTP-laag geeft 200 terug. De SOAP-envelope is intact. De binnenste <result> is <error>Session expired</error>, en een XML-parser die alleen de top-level nodes loopt, ziet dat niet. Check dus expliciet of result == "success".

7. AFAS GetConnector — skip-parameter loopt vast op 49.999

Boven 49.999 valt skip stilletjes terug op 0. Pagineer je door een HrEmployee-dataset van 200k records, dan verwerk je de eerste pagina vier keer. Gebruik orderbyfieldids met een stabiele cursor op een datum- of id-veld in plaats van offset-paginering.

8. Exact division-parameter — defaultet naar de verkeerde administratie

Laat je ?division= weg, dan pakt Exact de default van de gebruiker. Voor OAuth-serviceaccounts is die 'default' de eerste administratie die ooit op de tenant is aangemaakt — meestal een holding- of testadministratie, nooit de operationele. In onze eerste week belandden boekingen bij de verkeerde klant. Inmiddels laten we elk request zonder expliciete division hard falen.

Tier 3: faalt luidruchtig, te herstellen

9. Exact rate limits — reset op de klok, geen rolling window

60 requests per minuut op /current/Me en de meeste metadata-endpoints. De reset valt op de hele minuut, niet 60 seconden na je eerste request. Burst 60 om 10:00:59 en je mag er om 10:01:00 nog eens 60 doorheen jagen. De meeste rate-limit-clients gaan uit van een rolling window en wachten dus te lang.

10. Twinfield XML — namespace-prefixes gecheckt tegen een hard-coded tabel

Gebruik je <finance> in plaats van <twf:finance> met een default-xmlns gedeclareerd, dan wordt de hele batch geweigerd met Invalid envelope. Volgens de spec is de XML gewoon geldig. De parser van Twinfield checkt prefixes tegen een interne tabel en negeert de werkelijke namespace-binding.

11. AFAS Profit — tokens roteren elke 90 dagen, zonder waarschuwing

Profit-tokens verlopen na 90 dagen. Geen e-mail. Geen deprecation-header. Vrijdagmiddag geeft je integratie ineens 401's. Zet een agenda-herinnering op dag 80, of laat je code roteren via het token-management-endpoint.

12. Exact JSON-datums — Microsoft-formaat bij POST, ISO 8601 wordt geweigerd

GET geeft "/Date(1718841600000)/" terug. POST verwacht hetzelfde. ISO 8601 zoals 2026-06-20T00:00:00Z levert een 400 op met een generieke 'format error' die het veld niet eens noemt. We hebben er een hele dag aan verspild voordat we in de docs op de milliseconden-timestamps stuitten.

Tier 4: bekend, irritant, ergens gedocumenteerd

13. Twinfield — UTF-8-BOM in JSON-responses

Sommige endpoints zetten een byte-order mark voor de JSON. Go's encoding/json weigert dat. De meeste andere talen slikken het. Strip de eerste drie bytes als die EF BB BF zijn.

14. AFAS — ongedocumenteerde abonnement-gating geeft lege 403 terug

Endpoints zoals HrPerson vereisen een abonnementsvlag op de tenant. De 403-body is leeg — geen bericht, geen hint. Als een endpoint dat het altijd deed ineens stil is, check dan eerst de Profit-licentiepagina voordat je je auth-code begint te slopen.

15. Exact Modified-veld — UTC-suffix verplicht voor de concurrency-check

PUT vereist Modified als UTC met een Z aan het eind. Laat je de Z weg, dan ziet Exact het als Amsterdamse lokale tijd. Tijdens de zomertijd overschrijf je stilletjes een nieuwere wijziging of krijg je een 409 zonder uitleg. Stuur altijd Z mee.

16. Twinfield browse codes kappen af op 250 tekens

Browse codes (waarmee je een transactie op kenmerk vindt) worden stilletjes afgekapt bij 250 tekens. Lange inkooporder-omschrijvingen worden ingekort, de transactie is via de API niet meer te vinden terwijl ze in de UI gewoon zichtbaar is. De webservice-docs van Twinfield noemen de limiet op de browse-code-pagina, maar niet op het referentieveld waar het je in de praktijk pijn doet.

Hoe de verificatielaag eruitziet

Vanaf week drie vertrouwden we 200 OK niet meer. De boekhoud-agent doet nu dit voor iedere geboekte post:

def verify_posted(entry_id: str, expected: JournalEntry) -> None:
    # Re-read after 5 seconds to cover eventual consistency
    time.sleep(5)
    actual = client.get_journal_entry(entry_id)

    if actual.total_debit != expected.total_debit:
        alert("debit mismatch", entry_id)
    if actual.total_credit != expected.total_credit:
        alert("credit mismatch", entry_id)
    for line in actual.lines:
        if line.gl_account is None:
            alert("dropped grootboekrekening", entry_id, line.id)
        if line.vat_amount != expected.vat_for(line.id):
            alert("vat rounding", entry_id, line.id)

Het kost vijf seconden extra latency per post. Voor een nachtelijke batch van 800 inkoopfacturen is dat zo'n 70 minuten slapen op de klok. We draaien de verifier in een parallelle worker-pool en de hele batch is in twaalf minuten klaar. De agent boekt om 22:00, de verifier is om 22:30 klaar, en de controller opent om 08:00 haar mail — óf niets, óf een gesorteerde lijst met mismatches inclusief entry-ID's en directe links naar de UI van het pakket.

Wat we opgeleverd hebben, in twee zinnen

Toen we de boekhoud-agent bouwden voor het kantoor in Zwolle, was de zwartste week een BTW-aangifte-preview die €184 scheef stond over drie klanten — alles te herleiden tot quirk #1. We hebben het opgelost door de verificatielaag hierboven te schrijven en die in elke AI-agent in te bouwen die we leveren en die met een financiële API praat.

Draai je al een boekhoudintegratie? Dan is dit de audit van vijf minuten: trek de journaalposten van afgelopen maand uit je source-of-truth én uit het pakket, diff de regeltotalen per entry-id, tel de mismatches. Komt die teller op iets anders dan nul, dan zit er ergens in je stack een tier-1-quirk.

Kern

Vertrouw geen 200 OK meer van een financiële API. Lees iedere geboekte post terug en vergelijk de regels via een checksum met wat je verstuurde — dat is de enige betrouwbare verdediging.

FAQ

Welke quirk is in productie het gevaarlijkst?

Quirk #1 (Exact BTW-verlegd afronden bij deelcredits) en #2 (Twinfield grootboekrekening verdwijnt boven €5.000) komen allebei voorbij met een 200 OK en blijken pas bij de kwartaalaangifte. Verifieer altijd door terug te lezen.

Voegt de verificatielaag te veel latency toe voor real-time boekingen?

Voor nachtelijke batches niet, parallel draaiend kost het ongeveer tien minuten op de klok. Voor interactieve boekingen is het vijf seconden per post. De meeste boekhoudprocessen kunnen die vertraging hebben; flows die de gebruiker ziet, vragen een andere opzet.

Zijn deze quirks ergens officieel gedocumenteerd?

Sommige liggen begraven in forumdraden of release notes. De vier tier-1-quirks met stille corruptie zijn in de praktijk niet gedocumenteerd. AFAS, Exact en Twinfield hebben alle drie publieke docs, maar de stille faalmodi halen de officiële referentie zelden.

Gelden deze quirks ook voor Exact Online Premium of alleen voor Standard?

Quirks 1, 4, 5, 8, 9, 12 en 15 zagen we op zowel Standard- als Premium-tenants. Het REST-oppervlak van Exact is gedeeld tussen de edities, dus de meeste quirks reizen mee.

integrationsai agentsautomationcase studyworkflowoperations

Iets bouwen?

Start een project