← Blog

Integrations

AFAS Profit-connectoren: 17 valkuilen die 200 OK retourneren

Zeventien AFAS Profit-connector-valkuilen uit het onboarding-agent-project van een Apeldoorns payrollbureau van 38 personen, gerangschikt op hoe stil elk een multi-werkmaatschappij-tenant breekt.

Jacob Molkenboer· Oprichter · A Brand New Company· 13 jun 2026· 9 min
Koperen schakelbord met zeventien stekkers op ivoren bureaulegger, één groene patchkabel, gevouwen donker briefje, rode lakzegel.

De dinsdagochtend-misser in Apeldoorn

Een payrollbureau in Apeldoorn draait elke tweede dinsdag van de maand de loonrun voor 38 klanten, allemaal ondergebracht in één AFAS Profit-tenant. In februari opende hun HR-coördinator de verloningsperiode en stonden drie nieuwe medewerkers uit de Microsoft Forms-intake op geen enkele loonstaat. De onboarding-agent had voor alle drie 200 OK gelogd op de UpdateConnector. De records stonden in het systeem. Alleen niet in de juiste maand.

Die bug kostte ons vier uur reproduceren en twee dagen om alle andere connectoren die de agent raakte in kaart te brengen. Toen we klaar waren hadden we een cheatsheet van zeventien faalmodi die AFAS' eigen validatie passeren, gerangschikt op hoe stil ze een tenant met meerdere werkmaatschappijen breken.

Waarom een connector kan liegen over succes

De GetConnector- en UpdateConnector-REST-endpoints van AFAS Profit zijn code uit het SOAP-tijdperk met een JSON-jasje eroverheen. De server valideert het XML-schema, de veldtypes en de rechten van je token. Hij valideert niet of de payload zakelijk klopt voor de loonmodule die er verderop mee werkt. Een veld met de verkeerde code belandt gewoon in de database. Of het daarna een verloningsperiode aanstuurt, is een aparte vraag die de connector nooit stelt.

Op een tenant met één werkmaatschappij is dit nog vergevingsgezind. Er is maar één werkgever, één loonkalender, één set geldige periodecodes. Op een tenant met 38 werkmaatschappijen zijn dat er telkens 38. Elke valkuil hieronder vonden we toen de tenant van het bureau 200 OK retourneerde op een payload die de loonmodule vervolgens negeerde.

De vijf die de verloningsperiode laten vallen

Deze staan bovenaan omdat elke valkuil 200 OK retourneert op de UpdateConnector en de nieuwe medewerker stilletjes buiten de actieve verloningsperiode van zijn werkmaatschappij laat staan.

1. Wg valt standaard terug op werkgever één

Elke KnEmployee-payload die het Wg-veld weglaat, wordt weggeschreven naar werkgever 1, ongeacht welke werkmaatschappij de meegestuurde bedrijfscode bezit. Geen waarschuwing. Op een tenant met één werkmaatschappij is Wg een no-op. Op de tenant van het bureau belandden de eerste drie onboardings van de uitrol allemaal onder "Administratie Holding" in plaats van bij de werkmaatschappij. De agent-log toont voor elk 200 OK.

De fix: laat Wg nooit weg en haal hem op via een GetConnector-lookup op KnOrganisation in plaats van een hardgecodeerde map. Werkgevers krijgen een nieuw nummer als een bureau een nieuwe klant aanneemt.

<KnEmployee>
  <Element>
    <Fields Action="insert">
      <EmId>110621</EmId>
      <Wg>14</Wg>
      <Vp>202606</Vp>
      <PvCd>MAAND</PvCd>
    </Fields>
  </Element>
</KnEmployee>

2. Vp accepteert elke zescijferige code

Het verloningsperiode-veld accepteert elke correct opgemaakte YYYYMM-string. Het controleert niet of die periode openstaat op de werkmaatschappij die je aanspreekt. We zagen 202604 weggeschreven worden naar een werkmaatschappij waarvan de loonkalender al gesloten was voor Q1. Het record bleef in limbo hangen tot iemand de periode handmatig opende.

Lees de open periodes uit de InsiteCalendar-GetConnector vóór elke UpdateConnector-call. Cache één uur, geen dag.

3. PvCd matcht niet met de periodetabel per werkgever

PvCd is de periodecode. Het bureau had MAAND, VIERWEKEN en WEEK actief over verschillende werkmaatschappijen heen. MAAND meesturen naar een werkgever die alleen VIERWEKEN draait, wordt gewoon weggeschreven. De volgende loonrun laat de medewerker stilletjes vallen omdat de periodetabel voor die werkgever geen MAAND bevat.

4. MatchPer 7 negeert updates

Het MatchPer-attribuut op het KnPerson-element bepaalt hoe AFAS de inkomende persoon afstemt op bestaande records. De default in de meeste tutorials is 7, oftewel "insert new". Bij een hernieuwde onboarding voegt MatchPer 7 een duplicaat persoon-record toe en hangt de EmId aan de nieuwe BcCo. Het oude medewerkerrecord blijft staan. Beide calls retourneren 200 OK.

Gebruik MatchPer 0 met een expliciete BcCo-lookup, of MatchPer 6 als je het BSN-veld vertrouwt. Nooit 7 buiten een schone test-tenant.

5. CalculateBalance staat default op false

Het CalculateBalance-attribuut op KnEmployee triggert AFAS om na de write het loonsaldo van de medewerker opnieuw te berekenen. Staat hij uit, dan kan een medewerker netjes in de verloningsperiode van die werkmaatschappij belanden, maar met een nul-saldo waardoor hij niet zichtbaar is in de loonstaat-preview. De HR-coördinator kijkt naar de preview, ziet niets en meldt de onboarding als mislukt, terwijl het record er gewoon staat.

<Fields Action="insert" CalculateBalance="true">

De middenmoot-valkuilen

Deze laten de verloningsperiode niet stilletjes vallen, maar breken verderop dingen zonder error.

6. De token moet base64 zijn van een XML-envelope, niet de ruwe token

De AFAS-docs laten de token in plain text zien. Het endpoint verwacht base64 van <token><version>1</version><data>YOUR_TOKEN_HEX</data></token>. De ruwe hex-string sturen geeft 401. Base64 van de hex-string zonder XML-envelope geeft 200 OK op read endpoints en een stilletjes lege resultset op gefilterde endpoints. Wij verloren daar een middag aan.

7. Skip is one-indexed, ondanks de naam

Skip=0 geeft het eerste record. Skip=1 ook. Skip=2 geeft het tweede. Een off-by-one tegen elke gepagineerde reader die je ooit geschreven hebt. De incrementele sync van de agent had twee weken lang het eerste record van elke pagina overgeslagen voordat we het merkten.

8. Take piekt op 1000, zonder waarschuwing erboven

Take=10000 retourneert 1000 records. Geen header, geen hint dat er gepagineerd moet worden. Als je code niet controleert of het aantal resultaten gelijk is aan de take-waarde en doorloopt, stop je stilletjes met lezen bij 1000.

9. Boolean-velden willen "1", niet "true"

Het XML-schema noemt xs:boolean, maar de loonmodule leest xs:string. "true" doorgeven landt als letterlijke string, en elke downstream-filter die met 1 vergelijkt faalt. Datums idem: ISO YYYY-MM-DD wordt door de connector geaccepteerd en stilletjes verkeerd verwerkt in velden waar de module Nederlands formaat DD-MM-YYYY verwacht. Gebruik de AFAS-voorbeeld-XML voor het specifieke veld, niet het schema.

10. Filter-operators zijn integers, geen strings

De filter-syntax van de GetConnector gebruikt integer-codes voor operators. 1 is gelijk, 2 is groter dan, 3 is groter dan of gelijk aan, 8 is bevat, 14 is begint met. De lijst staat in de REST-connector-help onder "Filteren". De operator als string als "eq" doorgeven, geeft de ongefilterde resultset. 200 OK, volledige tabel, geen error.

11. KnPerson AddressLine wordt opgelost via PostBus-tabellen

Schrijf je een Nederlands adres met huisnummer 12A als één string, dan splitst AFAS het in HmNr=12 en HmAd=A. Schrijf je HmNr=12A direct, dan laat hij de A stilletjes vallen. De postcodeservice geeft dan een andere straat terug, het adres ziet er in de connector-echo correct uit, maar verwijst naar een ander pand.

De zes die je op dag één raakt

Dit zijn de luidruchtigere. Ze prikken je één keer, je fixt het veld, je gaat door.

  1. EmId-collisions bij parallel inserten. AFAS genereert EmId op de server. Twee parallelle UpdateConnector-calls voor twee verschillende nieuwe medewerkers kunnen dezelfde EmId teruggeven als ze binnen dezelfde seconde binnenkomen. Serialiseer het insert-pad of doe een post-check.
  2. De JobTitleId-lookup moet bestaan vóór de insert. Voeg een persoon toe met een functiecode die niet in de Functietabel van die werkmaatschappij staat en het veld landt als null. Geen error. De medewerker heeft geen functie.
  3. Trial-tenants throttlen op 50 calls per minuut. Productie throttlet op 600. De load-test van de agent op de trial-tenant van het bureau zag er prima uit. Productie zag er hetzelfde uit, totdat we onboarding-batches boven de 50 per minuut duwden en tegen een soft-block aanliepen die 20 minuten duurde om op te klaren.
  4. DaPaGsrt is een vijfletterige code, geen drieletterige. "MAAND", niet "MND". Het schema zegt max 5, maar de voorbeelden in oudere blogposts gebruiken 3. Drieletterige codes worden geaccepteerd en door de periode-engine genegeerd.
  5. Lege string is niet null. <HmNr></HmNr> sturen schrijft een lege string weg die downstream "is gevuld"-checks faalt. Laat het veld helemaal weg als je geen waarde hebt.
  6. De audit-log toont de request, niet de uiteindelijk weggeschreven data. De connector-log laat zien wat je stuurde. Niet wat AFAS schreef na het toepassen van default-waardes. Wil je de write verifiëren, dan moet je het record terug-GetConnectoren en diffen. Dat doet de agent nu bij elke insert boven een bepaalde confidence-drempel.
Let op

Leunt je UpdateConnector-pipeline op 200 OK als bewijs van succes op een tenant met meerdere werkmaatschappijen, dan heb je een silent-data-loss bug. Lees elke insert terug via een GetConnector en vergelijk Wg, Vp en PvCd voordat je het vertrouwt.

Het verificatiepatroon dat de bug dichtte

De fix die we voor het bureau opleverden was geen langere featurelijst. Het was een verificatiestap van één alinea die de agent na elke onboarding-insert draait.

def verify_employee_write(em_id: int, expected: dict) -> bool:
    record = afas.get(
        connector="Employees_full",
        filterfieldids="EmId",
        filtervalues=str(em_id),
        operatortypes="1",
        take=1,
    )
    if not record:
        return False
    actual = record[0]
    for field in ("Wg", "Vp", "PvCd"):
        if actual.get(field) != expected[field]:
            log.warn("afas-drift", em_id=em_id, field=field,
                     expected=expected[field], actual=actual.get(field))
            return False
    return True

De agent blokkeert nu op deze verificatie. Echoot de GetConnector een Wg, Vp of PvCd die niet matcht met wat de UpdateConnector had moeten wegschrijven, dan gaat de nieuwe medewerker in de wachtrij voor een menselijke review in plaats van dat de welkomstmail uitgaat. Twee maanden verder heeft het bureau geen verloningsperiode-misser meer gehad.

Hoe dit eruitziet op agent-niveau

Toen we de HR-onboarding-agent voor het bureau bouwden, was het model zelf het makkelijke deel. Het lastige was hem leren dat 200 OK geen contract is. Uiteindelijk hebben we elke AFAS-write verpakt in een read-back-stap met verwachtingen op veld-niveau, en de confidence-score van de agent telt connector-drift nu mee op dezelfde manier als ambiguïteit in formuliervelden. Draai je AI-agents tegen een Nederlandse ERP die al sinds het AccountView-tijdperk meegaat, ga er dan vanuit dat de connectoren beleefd liegen en verifieer alles wat ertoe doet.

Trek je UpdateConnector-logs van de afgelopen week en grep op 200 OK. Pak er tien willekeurig uit en haal het record op via de GetConnector. Is er ergens een veld op default gevallen, dan heb jij ook een silent-loss bug.

Kern

De UpdateConnector van AFAS Profit retourneert 200 OK op payloads die de loonmodule stilletjes negeert. Op tenants met meerdere werkmaatschappijen: lees elke Wg, Vp en PvCd terug.

FAQ

Geeft de UpdateConnector van AFAS Profit een error als een veld stilletjes wordt gedropt?

Nee. De connector valideert het schema en de rechten, geen businessregels. Verkeerde werkgevercodes, niet-matchende periodecodes en ontbrekende CalculateBalance-flags retourneren allemaal 200 OK, terwijl de loonmodule de regel negeert.

Hoe weet ik welke verloningsperiode open staat per werkmaatschappij?

Query de InsiteCalendar-GetConnector per Wg vóór elke UpdateConnector-write. Cache hem niet langer dan een uur; bureaus openen en sluiten periodes midden op de dag rond loonruns.

Waarom is de Skip-parameter van de GetConnector one-indexed?

Historisch SOAP-gedrag dat in het REST-jasje bewaard is gebleven. Skip=0 en Skip=1 geven beide record één terug. Behandel Skip als een one-indexed offset, anders verlies je de eerste regel van elke pagina die je leest.

Kun je silent UpdateConnector-failures detecteren zonder elk record terug te lezen?

Niet betrouwbaar. AFAS biedt geen post-write hash of echo van uiteindelijk weggeschreven velden. Het enige veilige patroon op tenants met meerdere werkmaatschappijen is een per-insert GetConnector-read-back op de velden die het loon aansturen.

integrationsai agentsprocess automationworkflowcase studyautomation

Iets bouwen?

Start een project