Build the Loonaangifte Payroll Declaration
Skill: Convert payroll data into the Dutch Loonaangifte XML
Region: Netherlands (Nederland)
Category: Payroll — Loonaangifte (loonheffingen) for the Belastingdienst
Does: Takes a period payroll run and produces the Loonaangifte XML — the periodic wage-tax-and-contributions declaration filed with the Belastingdienst, combining a collective return (Collectief) of period totals with a nominative return (Nominatief) of one record per employee keyed by BSN.
Standard: Belastingdienst Loonaangifte (Gegevensspecificaties Aangifte Loonheffingen) — XML message delivered over Digipoort / BAPI; the specification is reissued each tax year (2026 version).
The loonaangifte is a structured XML message lodged through Digipoort (or a certified payroll package) with PKIoverheid authentication — not a portal form. The Belastingdienst reconciles the
Collectieftotals against the sum of everyNominatiefemployee record, so the two must tie exactly. Code lists (sectoraansluiting, codes for Awf/Aof/Whk, incidentele inkomstenvermindering, etc.) and premium percentages change every year; validate against the current year's Gegevensspecificaties before sending. Amounts are in eurocenten (whole cents, no decimal point).
When this applies
- A Dutch inhoudingsplichtige (withholding agent / employer) files a loonaangifte for every aangiftetijdvak (usually a 4-weeks or calendar-month period) covering all employees on the payroll.
- The return is due and paid by the end of the month following the tijdvak (e.g. the March 2026 period → 30 April 2026); the declaration deadline and the payment deadline coincide.
- It reports both loonbelasting/premie volksverzekeringen (loonheffing) and the employer's premies werknemersverzekeringen (Awf, Aof, Whk) plus the inkomensafhankelijke bijdrage Zvw.
- Corrections to an already-filed period are sent as correctieberichten — a fresh
Nominatiefrecord for the affected BSN inside a later period's aangifte, with the original tijdvak referenced. - Excluded: a nihilaangifte (no wages) still files a
Collectiefwith zero totals and noNominatiefrecords.
Conversion procedure
- Read the source. Accept the payroll run as CSV/JSON (one row per employee: BSN, name, gross wage, loon LB/PH, loon SV, ingehouden loonheffing, premium bases) or a payroll-system export. Parse rows directly; OCR/
pdftotexta scanned loonstaat first. - Identify the employer and tijdvak. Capture the loonheffingennummer (BSN/RSIN +
L+ sub-number), the aangiftetijdvak (start/end dates), and the assigned sector/premium settings. - Build one
Nominatiefrecord per employee. Key each on the BSN (or, if no BSN, the personeelsnummer plus identifying data). Fill the inkomstenverhouding (income relationship) with loon SV, loon LB/PH, the ingehouden loonheffing, and the premium bases for Awf, Aof, Whk and Zvw. - Classify the Awf premium. Tag each income relationship as Awf hoog or Awf laag (low when there is a written, indefinite, non-on-call contract) — this drives the WW-Awf percentage applied.
- Aggregate the
Collectief. Sum the ingehouden loonheffing and each premie across allNominatiefrecords to produce the period totals and theTotaalGenAang(total payable). - Apply corrections. For a prior-period fix, emit a correction
Nominatiefreferencing the original tijdvak; never silently overwrite. - Emit the XML and validate. Build the
Loonaangiftemessage, then run the checklist; confirmCollectief= ΣNominatief.
Source → Loonaangifte field map
| From the source | → Target element |
|---|---|
| Employer loonheffingennummer | LoonaangifteTijdvak/Loonheffingennummer |
| Tijdvak start / end date | TijdvakAangifteBegindatum / TijdvakAangifteEinddatum |
| Total wage tax + national-insurance withheld | Collectief/IngehoudenLoonbelastingPVV |
| Total employer premies werknemersverz. | Collectief/PremieAwf + PremieAof + PremieWhk |
| Total Zvw employer contribution | Collectief/BijdrageZvw |
| Total payable for the period | Collectief/TotaalGeneraleAangifte |
| Employee BSN | Nominatief/Werknemer/BurgerservicenummerWerknemer |
| Employee initials / surname | Werknemer/Voorletters, SignificantDeelVanDeAchternaam |
| Date of birth | Werknemer/Geboortedatum |
| Income-relationship number | InkomstenverhoudingNr |
| Start of the income relationship | DatumAanvangInkomstenverhouding |
| Code soort inkomstenverhouding | CodeSoortInkomstenverhouding |
| Sector / sectorrisicogroep | Sector / Sectorrisicogroep |
| Loon for loonbelasting/volksverz. | LoonLoonbelastingVolksverzekeringen |
| Loon for werknemersverzekeringen (SV) | LoonSV |
| Loon for Zvw | LoonZvw |
| Withheld loonheffing | IngehoudenLoonbelastingPVVTabel |
| Awf high/low + base | AwfHoog / AwfLaag, PremieloonAWf |
| Aof premium (laag/hoog) base | PremieloonAOFLaag / PremieloonAOFHoog |
| Whk premium base | PremieloonWHK |
Document structure
Loonaangifte (root message)
└── Berichtgever (the submitter — name, contact)
└── AdministratieveEenheid
└── LoonaangifteTijdvak
├── Loonheffingennummer (NNNNNNNNNL01)
├── TijdvakAangifteBegindatum (yyyy-mm-dd)
├── TijdvakAangifteEinddatum (yyyy-mm-dd)
├── Collectief (period totals — exactly one)
│ ├── IngehoudenLoonbelastingPVV
│ ├── PremieAwf / PremieAof / PremieWhk
│ ├── BijdrageZvw
│ └── TotaalGeneraleAangifte
└── Nominatief (one per employee, repeatable)
└── Werknemer
├── BurgerservicenummerWerknemer (BSN)
├── Voorletters / Achternaam / Geboortedatum
└── Inkomstenverhouding (income relationship)
├── InkomstenverhoudingNr
├── CodeSoortInkomstenverhouding
├── LoonLoonbelastingVolksverzekeringen
├── LoonSV / LoonZvw
├── IngehoudenLoonbelastingPVVTabel
├── PremieloonAWf (+ AwfHoog | AwfLaag indicator)
├── PremieloonAOFLaag | PremieloonAOFHoog
└── PremieloonWHK
The Collectief block appears once per tijdvak; Nominatief repeats once per income relationship. A nihilaangifte carries the Collectief with zeros and no Nominatief. All amounts are integers in eurocenten.
Code tables
CodeSoortInkomstenverhouding (soort IKV, selected)
| Code | Meaning |
|---|---|
11 |
Loon of salaris ambtenaren in de zin van de Ambtenarenwet |
13 |
Loon of salaris directeuren van een nv/bv, wel verzekerd |
15 |
Loon of salaris niet onder te brengen onder andere codes |
18 |
Werknemer uitzendbureau |
31 |
Ouderdomspensioen (AOW/pensioen) |
Awf (WW-Awf) premium indicator
| Indicator | Meaning |
|---|---|
AwfHoog |
High WW-Awf percentage — flexibele/oproep/bepaalde-tijd contracts |
AwfLaag |
Low WW-Awf percentage — written, indefinite, non-on-call contract |
Premies werknemersverzekeringen (employer-paid)
| Premie | Stands for |
|---|---|
| Awf | Algemeen Werkloosheidsfonds (WW) — hoog or laag tarief |
| Aof | Arbeidsongeschiktheidsfonds — Aof laag (small employer) or Aof hoog |
| Whk | Werkhervattingskas (gedifferentieerde premie Whk) |
| Zvw | Inkomensafhankelijke bijdrage Zorgverzekeringswet (employer) |
Tijdvak (period) examples
| Tijdvak | Begindatum | Einddatum | Aangifte/betaal-datum |
|---|---|---|---|
| Maart 2026 (maand) | 2026-03-01 | 2026-03-31 | 2026-04-30 |
| Periode 03 2026 (4-weken) | 2026-02-23 | 2026-03-22 | 2026-04-30 |
Calculation rules
- Amounts are reported in eurocenten (integer cents). €3.520,00 →
352000. Never emit a decimal point. - Ingehouden loonheffing per employee = loonbelasting + premie volksverzekeringen withheld per the loonheffingstabel for that loon LB/PH.
- Awf premium =
PremieloonAWf× the year's WW-Awf percentage (hoog or laag per the contract indicator). - Aof premium =
PremieloonAOF*× the year's Aof percentage (laag for small employers, hoog otherwise). - Whk premium =
PremieloonWHK× the employer's gedifferentieerde Whk percentage (sector/own-risk dependent). - Zvw bijdrage =
LoonZvw× the year's Zvw werkgeversheffing percentage. Collectieftotals = Σ over allNominatiefrecords of each corresponding amount;TotaalGeneraleAangifte= loonheffing + Awf + Aof + Whk + Zvw for the period.- Recompute every total from the
Nominatiefrecords; theCollectiefmust reconcile exactly or the Belastingdienst rejects the message — flag any mismatch with the source.
Worked example (end-to-end)
Employer Voorbeeld Werkgever BV, loonheffingennummer 123456789L01, tijdvak maart 2026 (2026-03-01 → 2026-03-31), one employee: J. Jansen, BSN 111222333, born 1990-05-12, income relationship 1, soort IKV 15. Loon LB/PH €3.520,00, loon SV/Zvw €3.520,00, ingehouden loonheffing €820,00. Contract is permanent → Awf laag. Premies (already computed at the year's percentages): Awf €92,84, Aof €228,80, Whk €43,12, Zvw €232,32. Period totals equal this single employee's amounts; TotaalGeneraleAangifte = 820,00 + 92,84 + 228,80 + 43,12 + 232,32 = €1.417,08.
<?xml version="1.0" encoding="UTF-8"?>
<Loonaangifte xmlns="http://www.belastingdienst.nl/schemas/loonaangifte/2026">
<Berichtgever>
<Naam>Voorbeeld Werkgever BV</Naam>
<AdministratieveEenheid>
<LoonaangifteTijdvak>
<Loonheffingennummer>123456789L01</Loonheffingennummer>
<TijdvakAangifteBegindatum>2026-03-01</TijdvakAangifteBegindatum>
<TijdvakAangifteEinddatum>2026-03-31</TijdvakAangifteEinddatum>
<Collectief>
<IngehoudenLoonbelastingPVV>82000</IngehoudenLoonbelastingPVV>
<PremieAwf>9284</PremieAwf>
<PremieAof>22880</PremieAof>
<PremieWhk>4312</PremieWhk>
<BijdrageZvw>23232</BijdrageZvw>
<TotaalGeneraleAangifte>141708</TotaalGeneraleAangifte>
</Collectief>
<Nominatief>
<Werknemer>
<BurgerservicenummerWerknemer>111222333</BurgerservicenummerWerknemer>
<Voorletters>J</Voorletters>
<SignificantDeelVanDeAchternaam>Jansen</SignificantDeelVanDeAchternaam>
<Geboortedatum>1990-05-12</Geboortedatum>
<Inkomstenverhouding>
<InkomstenverhoudingNr>1</InkomstenverhoudingNr>
<DatumAanvangInkomstenverhouding>2018-01-01</DatumAanvangInkomstenverhouding>
<CodeSoortInkomstenverhouding>15</CodeSoortInkomstenverhouding>
<LoonLoonbelastingVolksverzekeringen>352000</LoonLoonbelastingVolksverzekeringen>
<LoonSV>352000</LoonSV>
<LoonZvw>352000</LoonZvw>
<IngehoudenLoonbelastingPVVTabel>82000</IngehoudenLoonbelastingPVVTabel>
<AwfLaag>J</AwfLaag>
<PremieloonAWf>352000</PremieloonAWf>
<PremieloonAOFLaag>352000</PremieloonAOFLaag>
<PremieloonWHK>352000</PremieloonWHK>
</Inkomstenverhouding>
</Werknemer>
</Nominatief>
</LoonaangifteTijdvak>
</AdministratieveEenheid>
</Berichtgever>
</Loonaangifte>
Normalisations shown: amounts to eurocenten (€3.520,00 → 352000, €820,00 → 82000, €1.417,08 → 141708); permanent contract → AwfLaag = J; Collectief/TotaalGeneraleAangifte recomputed 82000 + 9284 + 22880 + 4312 + 23232 = 141708; each Collectief total equals the sum of the single Nominatief record. This message is wrapped in the Digipoort aanlevering with PKIoverheid authentication.
Validation checklist
- All employees captured; AI asked about anything missing or ambiguous (no invented BSNs, wages, or premiums)
- Employer
Loonheffingennummerand the tijdvakBegindatum/Einddatumcorrect for the period - Exactly one
Collectief; oneNominatiefper income relationship, each keyed by a valid 9-digit BSN - Every amount in eurocenten (integer cents, no decimal point)
- Each income relationship tagged
AwfHoogorAwfLaagper the contract; premium bases populated for Awf/Aof/Whk/Zvw -
CodeSoortInkomstenverhoudingand sector codes valid for the current year -
Collectieftotals = Σ of theNominatiefrecords (loonheffing, Awf, Aof, Whk, Zvw);TotaalGeneraleAangiftereconciles - Corrections sent as correctie
Nominatiefreferencing the original tijdvak (prior period not overwritten) - XML well-formed; default namespace and element names match the current Gegevensspecificaties
- Declaration and payment lodged by the end of the month after the tijdvak; validated against the current-year Loonaangifte specification before Digipoort submission
Last updated: 2026-06-13 — verify the current-year Belastingdienst Gegevensspecificaties Aangifte Loonheffingen (element names, code lists, Awf/Aof/Whk/Zvw percentages) and Digipoort delivery rules before use.