Integrations
Twinfield API: 15 stille valkuilen achter een 200 OK
Twinfield geeft 200 OK terug terwijl het stilletjes cost centers laat vallen op multi-administratie tenants. Vijftien valkuilen uit een Rotterdamse agent-uitrol.

Het Slack-bericht kwam binnen om 14:47 op een donderdag. "Twinfield zegt dat de facturen geboekt zijn. De Twinfield UI laat ze zien. Bij 73 ervan is het cost center leeg." We waren drie weken bezig met de uitrol van een invoice-coding agent bij een Rotterdams boekhoudkantoor met 84 klantadministraties. De agent draaide al negen dagen, classificeerde gescande PDF's, haalde regels eruit en boekte ze in Twinfield via de SOAP transaction API. Elke response kwam terug als 200 OK. Elke transactie was te vinden in de UI. De dimensies die we aan elke regel hadden gehangen, degene die mappen naar cost centers en projecten, waren verdwenen op ongeveer een derde van de boekingen.
Dit is de cheatsheet die we op dag één hadden willen hebben. Vijftien Twinfield SOAP en REST quirks, gerangschikt op hoe stil ze dingen breken op multi-administratie tenants. Nummer één is de stille cost-center drop. De rest staat daaronder, ongeveer in de volgorde waarin ze je nachtrust gaan kosten.
Ga je dit kwartaal met Twinfield integreren? Bouw hier een checklist omheen. Zet je er een autonome agent op? Behandel elke 200 OK als een 'misschien'.
De stille-drop categorie
Deze geven 200 OK terug en laten je een dag debuggen. Ze delen een patroon: Twinfield valideert wat het begrijpt en gooit weg wat het niet begrijpt, zonder een fault te raisen.
1. Dimension codes die niet bestaan in de doeladministratie
Cost centers (het dim2 veld), projecten (dim3) en elke custom dimension zijn scoped per administratie, niet per tenant. Als je een transactieregel POST met <dim2>KP-040</dim2> en KP-040 bestaat niet in dat specifieke kantoor, wordt de regel opgeslagen zonder de dimension. Geen fault, geen warning, geen msg attribute op de regel. De transactie is "valid" omdat dim2 technisch optioneel is in het schema. Elk van de 84 administraties van het Rotterdamse kantoor had zijn eigen cost-center catalogus, geërfd van welke vorige boekhouder de file ook had ingericht. Onze agent stuurde hoofdkantoor-codes naar elk van ze.
2. Dimensions die bestaan maar geblokkeerd of inactief zijn
Twinfield laat boekhouders dimensions blokkeren na het jaareinde, of ze als inactief markeren voor nieuwe boekingen. Een geblokkeerde dimension op een geboekte regel: silent drop. Dezelfde status code, dezelfde response shape. De block-status laat zich niet zien in het standaard dimension-list endpoint, tenzij je expliciet om inactieve entries vraagt.
3. BTW-codes die bestaan maar niet gekoppeld zijn aan het transactietype
Elk transactietype (verkoopfactuur, inkoopfactuur, memoriaal, bank) heeft een allowlist van BTW-codes in de instellingen van de administratie. Stuur een geldige BTW-code die niet op de allowlist staat voor inkoopfacturen en de regel wordt geboekt met BTW-code null. De regels blijven sluiten omdat Twinfield het bedrag exclusief BTW berekent als de volledige regelwaarde. We vingen dit pas op toen de maandelijkse BTW-aangifte ineens 0,4% niet meer aansloot.
4. Line numbers die botsen binnen een multi-line transactie
In manual document-number mode geldt: hergebruik je een linenumber waarde binnen een transactie, dan overschrijft de latere regel de eerdere. Geen collision error.
<!-- Quietly drops line 1 -->
<line type="detail">
<linenumber>1</linenumber>
<dim1>4000</dim1>
<value>100.00</value>
</line>
<line type="detail">
<linenumber>1</linenumber>
<dim1>4001</dim1>
<value>200.00</value>
</line>
5. Hiërarchieniveaus die niet matchen met de inrichting van de administratie
Sommige administraties gebruiken alleen dim2. Sommige dim2 + dim3. Sommige definiëren een custom dimension op dim4 voor projectcodes. Het SOAP-schema accepteert ze allemaal op elke regel. Heeft de doeladministratie geen dim3 enabled, dan verdwijnt je dim3-waarde. Twinfield geeft 200 OK terug plus een transactie die er schoon uitziet.
De cluster- en session-categorie
Het auth-model van Twinfield is een subcultuur op zich. Geen van deze geeft je een 200 OK die liegt. Ze vreten wel je ochtend op als je ze in productie ontdekt.
6. De cluster-URL is per session, niet per tenant
Wanneer je inlogt via SOAP, bevat de response een <cluster> URL (bijvoorbeeld https://c3.twinfield.com). Alle vervolgcalls moeten naar die URL, niet naar de login-URL, en niet naar welk cluster je gisteren ook kreeg. Hardcode je c3 omdat dat vorige week werkte, dan praat je vandaag tegen een session op een ander cluster en krijg je Logon failed ondanks een verse token.
7. OAuth refresh tokens roteren bij elk gebruik
De REST API gebruikt OAuth2 met roterende refresh tokens. Elke refresh geeft een nieuwe refresh token terug, en de oude wordt direct ongeldig. Als twee workers tegelijk refreshen, houdt één van ze binnen milliseconden een ongeldige token vast. Na de tweede storing hebben we refreshes geserialiseerd achter één mutex per environment.
8. SelectCompany is niet zo idempotent als je zou hopen
Een aanroep van SelectCompany wisselt de actieve administratie voor de rest van de session. Vergeet je 'm aan te roepen na een logon, dan gaan operaties naar het default office van de user. Het default office op een service account is zelden degene die je wilt, en de API boekt vrolijk een factuur van een Rotterdamse klant in de boeken van het hoofdkantoor.
9. Concurrent sessions per user zijn gecapped
De cap staat niet prominent in de documentatie. Hij ligt rond de 10 tot 20, afhankelijk van je contract. Raak je 'm, dan krijg je Maximum number of concurrent sessions exceeded. Pool sessions per administratie en hergebruik ze. Open er geen per HTTP request.
De payload-shape categorie
10. SOAP geeft 200 OK met een SOAP Fault in de body
Dit is canonical SOAP, maar het verrast engineers die uit REST komen. Een <soap:Fault> element binnen een 200-response is nog steeds een error. Parse de XML voordat je de call als geslaagd behandelt.
resp = requests.post(cluster_url, data=envelope, headers=headers)
if resp.status_code != 200:
raise TwinfieldError(resp.text)
root = ET.fromstring(resp.text)
ns = {"s": "http://schemas.xmlsoap.org/soap/envelope/"}
fault = root.find(".//s:Fault", ns)
if fault is not None:
raise TwinfieldError(fault.find("faultstring").text)
11. De transaction-write response heeft per-line result attributes
Een transactie kan op het hoogste niveau opslaan met result="1" terwijl individuele regels result="0" en een msg attribute hebben. We hebben dit gezien toen één van drie regels validatie faalde en Twinfield de andere twee wegschreef. Loop de response tree na. Vertrouw het top-level resultaat niet.
12. Datumformaat is YYYYMMDD in SOAP, ISO-8601 in REST
Haal je ze door elkaar, dan zet SOAP ongeldige datums stilletjes om naar de eerste dag van de huidige periode. Je komt erachter wanneer de boekhouder vraagt waarom elke factuur uit mei op de 1e is geboekt.
13. Field-name casing verschilt tussen SOAP en REST
invoiceNumber in REST, invoicenumber in SOAP. Office codes zijn case-sensitive bij writes maar tolerant bij sommige reads. We hebben overal gestandaardiseerd op uppercase office codes nadat een agent naar office nl001 schreef terwijl de canonical NL001 was. Hetzelfde office in sommige lookups, een ander office in andere.
De coverage-gap categorie
14. De REST API dekt niet wat de SOAP API dekt
Per medio 2026 zijn journal-line dimension manipulatie, een aantal master-data writes en een handvol reporting endpoints alleen via SOAP beschikbaar. De Twinfield webservices documentatie is de source of truth voor wat elk oppervlak ondersteunt. De REST-docs suggereren meer pariteit dan er feitelijk is. Plan voor een hybrid client en kies het oppervlak per call, niet per project.
15. Periodes moeten open zijn, en de error noemt de administratie niet
Boeken in een gesloten periode geeft een duidelijke error terug, maar het bericht verwijst naar "period status" zonder je te vertellen welke administratie z'n periode dicht heeft. Als je over 84 administraties heen itereert, log dan de office code bij elke error. Anders zoek je blind in de cluster-logs.
Bij elke multi-administratie Twinfield-uitrol: schrijf een smoke test per administratie die een transactie van één regel met een bekend cost center boekt, 'm terugleest en bevestigt dat het cost center is meegegaan. Draai 'm 's nachts over elk kantoor. De silent drops uit categorie één laten zich niet zien in welke andere monitor je al hebt.
Wat we in de agent hebben aangepast
Nadat de cost-center drop boven kwam, hebben we drie lagen voor elke write gezet:
- Een pre-flight resolver die elke dimension, BTW-code en period status opzoekt tegen de master data van de doeladministratie, en de transactie weigert te sturen als één referentie niet opgelost is. Lookups worden per administratie gecachet met een TTL van 10 minuten.
- Een read-back stap na elke write die de transactie opnieuw ophaalt aan de hand van het teruggegeven nummer en de dimensions, BTW-codes en bedragen per regel diff't tegen de bedoelde payload. Drift triggert een Slack-alert met de office code, het transactienummer en het veld dat dreef.
- Een circuit breaker per administratie. Komt de read-back drift over een rolling uur boven de 2%, dan pauzeert de agent writes naar dat kantoor en gaat de rest in de queue voor menselijke review.
De derde laag is de les uit het bredere agent-gesprek van dit moment. Het patroon onder de meeste agent post-mortems is hetzelfde: een agent die bleef handelen waar hij had moeten stoppen. Bij boekhoudkoppelingen zijn de kosten van fout handelen concreet en auditbaar, dus de lat ligt niet bij "halt on error". Hij ligt bij "halt on uncertainty". Een read-back diff is het goedkoopste signaal voor onzekerheid dat we hebben gevonden.
Aan de orchestratie-kant modelleren libraries zoals Burr agentgedrag als een expliciete state machine, en die framing heeft veranderd hoe wij integraties bedraden. Elke Twinfield write gaat van drafted naar posted naar verified, en alleen verified sluit de loop. posted zonder verified betekent dat de API ja zei, maar dat we nog niet bewezen hebben dat de velden zijn aangekomen. Dat onderscheid is het hele verhaal.
Behandel elke Twinfield 200 OK op een multi-administratie write als voorlopig, totdat je het record hebt teruggelezen en de velden die ertoe doen hebt gediff't. De API slaat op wat hij begrijpt en gooit weg wat hij niet begrijpt.
Slot
Toen we de invoice-coding agent voor het Rotterdamse kantoor bouwden, liepen we er steeds tegenaan dat het idee van "valid" bij Twinfield smaller is dan jouw idee van "correct". We hebben het uiteindelijk opgelost met de pre-flight plus read-back loop hierboven, die nu in elke AI-agent zit die we tegen boekhoud-API's uitrollen.
Het kleinste nuttige dat je vandaag kan doen, als je deze maand met een Twinfield-integratie begint: schrijf de smoke test per administratie uit quirk één en zet 'm op een cron van 15 minuten. De dag dat hij rood gaat, is de dag dat je precies weet welke cost-center catalogus van welke administratie is gedreven, en welke agent-run de troep heeft veroorzaakt.
Kern
Behandel elke Twinfield 200 OK op een multi-administratie write als voorlopig, totdat je het record hebt teruggelezen en de velden die ertoe doen hebt gediff't.
FAQ
Dekt de REST API van Twinfield alles wat de SOAP API dekt?
Per medio 2026 niet. Journal-line dimension manipulatie, een aantal master-data writes en sommige reporting endpoints blijven SOAP-only. Plan voor een hybrid client en kies het oppervlak per call.
Waarom geeft Twinfield 200 OK terug als hij mijn cost center laat vallen?
Dimension-velden zijn technisch optioneel in het transaction-schema. Bestaat de code die je stuurde niet, of is hij geblokkeerd in die administratie, dan wordt de regel zonder de code opgeslagen. Lees de transactie altijd terug en diff de dimensions.
Hoe ga ik om met Twinfield OAuth refresh tokens over meerdere workers?
Refresh tokens roteren bij elk gebruik, dus concurrent refreshes maken elkaar ongeldig. Serialiseer refreshes achter één mutex per environment, of laat ze lopen via een toegewijd token-broker proces.
Wat is de veiligste manier om een Twinfield-integratie over veel administraties uit te rollen?
Bouw een smoke test per administratie die een transactie van één regel met een bekend cost center boekt, 'm terugleest en bevestigt dat de dimension is meegegaan. Draai 'm op een cron van 15 minuten en alarmeer op elke drift.