← Blog

Integrations

Facturen-OCR: vijftien valkuilen die je agent breken

Een boekhouder vindt negen facturen geboekt als alleen voorblad. De intake-agent werkt prima. De OCR-export niet. Dit zijn de vijftien quirks die het veroorzaken.

Jacob Molkenboer· Oprichter · A Brand New Company· 15 jun 2026· 9 min
Crème factuur met omgevouwen hoek, koperen paperclip, groene zijden draad, rood wax zegel op ivoor papier.

Het is dinsdagochtend bij een middelgroot Amsterdams accountantskantoor. De boekhouder opent negen facturen die de intake-agent in het weekend in Exact heeft geboekt. Acht zien er prima uit. De negende is een voorblad van KPN, met een beleefde notitie dat je factuur in de bijlage zit. Bedrag: nul. Regels: geen. De echte factuur staat op pagina twee en drie van dezelfde PDF.

De agent heeft niets verzonnen. Klippa gaf netjes een JSON-object terug voor pagina één en stopte daar, omdat de voorpagina werd geclassificeerd als een apart document binnen die PDF, en pagina twee en drie waren toegewezen aan een zusterobject waar de agent nooit naar vroeg.

Dit is het ergste soort fout. Hij gooit geen exception. Hij geeft geen waarschuwing. Hij boekt een rommelregel in het grootboek, en je komt erachter in de maandafsluiting wanneer een finance lead vraagt waarom de telefoonrekening op nul staat.

We hebben factuurintake-agents uitgerold bovenop Klippa, Basecone en DigiOffice bij accountantskantoren in Utrecht, Eindhoven en over de grens in Antwerpen. Op het moment van schrijven draaien er veertien agents in productie. Hieronder staan de vijftien export-quirks die elke uitrol bijten, gerangschikt op hoe stil ze het grootboek corrumperen.

De volgorde doet ertoe. Quirks die een error gooien zijn prima; die vang je en zet je in een wachtrij. Quirks die stilletjes een veld droppen of een document afkappen, dat zijn de quirks die van een OCR-pipeline een trage lek maken die niemand een kwartaal lang opmerkt.

Tier 1: stille afkapping van PDF's met meerdere pagina's

Drie van de vijftien staan bovenaan omdat ze echt geld kosten voordat iemand het symptoom ziet.

1. Klippa's multi-document-splitsing. Wanneer je een PDF uploadt waarvan Klippa's classifier denkt dat er meerdere documenten in zitten (begeleidende brief plus factuur, overzicht plus factuur, twee facturen aan elkaar geniet), wikkelt de standaard API-response elk document in een apart document-object binnen documents[]. Leest je agent alleen documents[0], dan heb je het voorblad geboekt en de factuur genegeerd. De fix is iteratie. De val is dat dat veld überhaupt bestaat en dat de meeste starter-code in hun dashboard index nul leest.

2. Basecone's max-pages-per-stream-limiet. Standaard-accounts beperken streaming OCR tot 30 pagina's per document. Alles daarboven wordt geparkeerd in een review-wachtrij en streamt nooit naar de agent. We hebben geconsolideerde leveranciersoverzichten van zestig pagina's drie weken lang in die wachtrij zien verdwijnen, omdat het enige signaal een tabblad in de UI was dat niemand opent.

3. DigiOffice ZIP-exports gebruiken Windows-backslashes in de manifest-paden. Bij een Linux-extractie (en dat zijn de meeste agent-runtimes) wordt het bestand aangemaakt als de letterlijke string invoices\2026\06\KPN-inv.pdf in de unzip-root, niet genest. Je agent leest de manifest, zoekt naar invoices/2026/06/KPN-inv.pdf, en slaat het over. De PDF staat er gewoon, onder een andere naam, in een andere map.

Tier 2: het stille velddrop

Deze verliezen geen pagina. Ze verliezen een kolom. De rest van de regel komt door elke check heen.

4. Basecone dropt de IBAN als het BIC-veld leeg is. Dit is dé klassieker. Hun export-serialiser behandelt IBAN en BIC als een betalingspaar; is BIC leeg (heel gebruikelijk bij SEPA-betalingen binnen NL, waar BIC optioneel is onder ISO 13616), dan klappen beide velden in de JSON terug naar null. De data staat in hun UI. Alleen niet in de export. We zagen het door te reconciliëren tegen de ruwe PDF-tekst en ontdekten dat 7% van de leveranciersregels de IBAN miste, terwijl OCR hem wel correct had geëxtraheerd.

Let op

Test de Basecone-export tegen een SEPA-factuur zonder BIC voordat je hem in de buurt van het grootboek laat. De gedropte IBAN is onzichtbaar in de JSON en het veld is wel gevuld in hun webapp, dus een handmatige steekproef vangt het niet.

5. Klippa kapt leveranciersnamen af op 35 tekens in het supplier.name-veld, terwijl de volledige string in supplier.raw_name staat. De starter-code leest de eerste. Je grootboekrekeningen krijgen vervolgens een fuzzy match op de afgekapte string, en er worden nieuwe leveranciersrecords aangemaakt waar bestaande hadden moeten matchen. Na een jaar heb je drie regels voor hetzelfde telecombedrijf, waarvan er geen enkele reconcilieert.

6. DigiOffice exporteert BTW-tarieven als strings zoals "21%" met een letterlijk procentteken, soms met een spatie erachter ("21% "). Cast je agent direct naar float zonder te strippen, dan faalt om de andere factuur de BTW-totaalcheck.

Tier 3: schema-drift op de zijpaden

Deze quirks treffen je niet op het happy path. Ze treffen je wanneer een factuur in een zijtak van de pipeline van de leverancier belandt, en het schema onder je voeten verandert.

7. Klippa wijzigt de JSON-vorm bij handmatige review. Het line_items-veld, normaal een array van objecten, wordt een object met de regelindex als key zodra een reviewer een opmerking toevoegt. Hetzelfde endpoint, dezelfde auth, andere vorm. Gebruikt je agent een strikte schema-validator, dan gooit hij een exception. Gebruikt hij een lakse, dan itereert hij over het object en krijgt hij keys in plaats van items, waarna de regeltotalen leeg blijven.

8. De webhook van Basecone vuurt twee keer voor facturen boven ~3MB. De eerste keer is status: "received", de tweede status: "extracted". Het gat is 200ms tot 4s afhankelijk van de belasting. Er is geen volgorde-garantie. Verwerkt je agent op de eerste call, dan zijn de geëxtraheerde velden leeg. Verwerkt hij op beide, dan boek je dubbel. Pollen is veiliger dan de webhook voor alles boven die drempel.

9. DigiOffice probeert mislukte OCR-runs opnieuw op de backend maar verhoogt geen versie-veld. Je kunt twee keer hetzelfde document_id GETten en andere inhoud terugkrijgen. Het enige signaal is de last_modified_at-timestamp, die de meeste agents bij een GET negeren. Houd 'm bij.

Tier 4: encoding, locale en andere kleine sneetjes

De overgebleven zes zijn de kleine irritaties die off-by-one-fouten in financiële rapportages opleveren.

10. DigiOffice CSV-exports gebruiken CP1252, geen UTF-8. Open zo'n bestand met Python's standaard open() op Linux en elk euroteken wordt een mojibake-stapel. Geef encoding="cp1252" mee en de wereld stabiliseert. Het staat in de docs, maar je moet 't wel vinden. Zie de CP1252-referentie voor de byte-voor-byte verschillen met UTF-8 in het hoge bereik.

11. Basecone exporteert datums als DD-MM-YYYY voor NL-accounts en als MM/DD/YYYY voor accounts waar ooit een Amerikaanse gebruiker aan is toegevoegd, ook al was het tijdelijk. De gebruiker wordt verwijderd. De locale-omslag blijft. We hebben dit één keer gezien, maar dat kantoor was er twee dagen zoet mee.

12. Klippa geeft confidence per veld terug maar markeert niet wanneer een hele pagina 90 graden gedraaid staat en de OCR vooral ruis is. De confidence op losse velden kan op 0,7 tot 0,9 zitten terwijl het document onleesbaar is. Tel de confidences over de pagina op; ligt het gemiddelde onder 0,85, kijk dan naar het origineel.

13. DigiOffice CSV gebruikt puntkomma als scheidingsteken (Excel-NL standaard) maar escapet geen puntkomma's binnen omsloten omschrijvingsvelden. Je parser zit off-by-one bij elke regel met een omschrijving die een letterlijke ; bevat. Leveranciersomschrijvingen zijn dol op puntkomma's.

14. Klippa's total_amount is inclusief BTW. Het netto bedrag staat in total_amount_excl. Beide worden teruggegeven. Pak je de verkeerde, dan boek je een jaar lang bruto als netto. We hebben het in productie gezien. De reconciliatie sloeg alleen aan omdat de BTW-aangifte-automatisering verderop in de keten BTW probeerde te herberekenen op een getal dat 'm al bevatte.

15. Basecone draait het teken om op BTW-regels van creditnota's wanneer de BTW-code een van drie legacy-codes is (NL-NUL, NL-VRIJ, NL-NIH). Het eindtotaal klopt alleen omdat de BTW-regel sowieso nul is. Op het moment dat je een niet-nul BTW op een creditnota hebt (zeldzaam in NL maar reëel bij sommige horecaklanten), is de export stilletjes afwijkend.

De vijfminuten-audit die je vandaag kunt draaien

Haal de facturen van de laatste zestig dagen door je pipeline en draai drie checks per regel. Je hoeft nog niets te fixen. Je moet weten wat je hitrate is.

import re

def audit_invoice(extracted, source_pdf_text, source_page_count):
    issues = []

    # Tier 1: did we read all pages?
    if extracted.get("page_count", 0) < source_page_count:
        issues.append("truncated")

    # Tier 2: critical fields present in source but missing in export?
    iban_in_source = re.search(r"NL\d{2}[A-Z]{4}\d{10}", source_pdf_text)
    if iban_in_source and not extracted.get("iban"):
        issues.append("iban_dropped")

    # Tier 4: does the total reconcile?
    try:
        net = float(str(extracted["total_amount_excl"]).rstrip("% ").strip())
        vat = float(str(extracted["vat_amount"]).rstrip("% ").strip())
        gross = float(str(extracted["total_amount"]).rstrip("% ").strip())
        if abs(net + vat - gross) > 0.02:
            issues.append("reconciliation_break")
    except (KeyError, ValueError):
        issues.append("unparseable_amounts")

    return issues

Honderd facturen, drie checks. Zit je truncated- of iban_dropped-teller boven de 2%, dan heb je een Tier 1- of Tier 2-quirk in het wild en een getal waar finance zich druk om gaat maken. Zit je reconciliation_break-teller boven de 5%, dan loop je waarschijnlijk tegen een Tier 4-verwarring met bedragvelden aan.

Waarom deze quirks überhaupt bestaan

Het patroon zit in alle vijftien hetzelfde. Elke leverancier bouwde een export-schema rondom een UI-workflow, niet rondom een geautomatiseerde lezer. De handmatige-review-array wordt een object omdat de UI op die manier comments rendert. De IBAN valt weg omdat het UI-formulier IBAN en BIC als één widget behandelt. Het voorblad splitst omdat de UI een reviewer toestaat elke pagina opnieuw te classificeren.

Niets hiervan is aan de leverancierskant op te lossen zonder bestaande exports van hun klanten te breken. De fix zit tussen de export en de agent in: een kleine reconciliatielaag met één taak. Bewijs dat wat de agent op het punt staat te schrijven matcht met wat op de bron-PDF staat, en zet alles wat dat niet doet in een wachtrij.

Toen we vorig kwartaal de factuurintake-agent bouwden voor een Utrechts accountantskantoor, liepen we tegen de Basecone IBAN-drop aan op 7% van de leveranciersregels. We hebben uiteindelijk één regex-pass over de bron-PDF-tekst na extractie toegevoegd, vergeleken met de export, en delta's naar een menselijke wachtrij gestuurd. Vijf uur werk ving een jaarlijkse boekingsfout met zes cijfers af. Dat soort lijmwerk is het leeuwendeel van hoe ons werk rond AI-agents er in productie uitziet.

Pak vandaag één verdachte factuur uit afgelopen week. Diff de export-JSON met de hand tegen de ruwe PDF-tekst. Binnen tien minuten weet je of je een Tier 1-probleem of een Tier 4-probleem hebt, en dat vertelt je welke week werk je in te plannen hebt.

Kern

Reconcilieer de export van je OCR-leverancier tegen de ruwe PDF-tekst. Stille afkapping en stille velddrops slopen meer factuurintake-agents dan slechte OCR ooit deed.

FAQ

Welke OCR-leverancier gaat out of the box het beste om met PDF's van meerdere pagina's?

Geen van allen is bugvrij. Klippa heeft het schoonste schema, maar de split-document-val is echt. De fix is een reconciliatie-stap op pagina-aantallen, niet een leverancierswissel.

Hoe spot je de Basecone IBAN-drop zonder elke PDF te lezen?

Regex de bron-PDF-tekst op het Nederlandse IBAN-patroon. Is er een match en is het iban-veld in de export null, markeer de regel dan voor een menselijke wachtrij.

Is deze lijst specifiek voor Nederlandse accountancy of geldt 'ie ook in andere markten?

De meeste zijn leveranciersgedrag, geen marktquirks. Klippa is Nederlands maar wordt internationaal gebruikt. De CP1252-val van DigiOffice raakt iedereen buiten West-Europa.

Waarom bel je de leverancier niet om de bugs te melden?

Hebben we gedaan. Een paar zijn over de jaren gefixt. De meeste staan gedocumenteerd als 'by design' omdat het veranderen ervan bestaande klantexports zou breken. Bouw die reconciliatielaag.

Hoe vaak moet je het audit-script in productie draaien?

Dagelijks gedurende de eerste maand na livegang, daarna wekelijks. Leveranciers-schema's veranderen zonder aankondiging wanneer ze features uitrollen, en het eerste signaal is meestal reconciliatie-drift, geen error.

integrationsai agentsautomationprocess automationworkflowoperations

Iets bouwen?

Start een project