// developers/ generators

Dutch test data, from a script.

The BRP/GBA and UPA generators over REST and MCP. Every setting on the tool page is a field in the request body, and the same seed always returns the same data.

Why you would call these instead of clicking.

Three reasons, and they are different from each other.

Test automation. A seed fixes the output, so a suite can assert on a specific BSN and still pass next month. Rebuild your fixtures from scratch in CI instead of committing a CSV that slowly stops matching your schema.

Development. Point your seed script at the endpoint and get a filled database in one call, including the awkward cases: names with diacritics, addresses abroad with no Dutch postcode, house number additions, people who died mid-period. These are the records that break an import, and they are the ones nobody types by hand.

Reliable AI output. Ask a language model for Dutch test data and it will produce BSNs that look right and fail the elfproef, because it is guessing digits. Over MCP the model calls the generator instead, so the numbers validate. The same holds for UPA XML, which no model can produce correctly from memory.

Both endpoints need a free API key. Get one on the developers page, or add the MCP server to your assistant.

POST /api/v1/tools/brp-test-data-generator

BRP and GBA test persons.

Returns people with valid BSNs, families, addresses and a life event each: married, widowed, divorced, deceased, living abroad, no fixed address, under investigation. Up to 5,000 per call, the same ceiling as the tool page.

· Minimal request
curl -X POST https://toolforte.com/api/v1/tools/brp-test-data-generator \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key_here" \
  -d '{}'
· 25 people near retirement, repeatable
curl -X POST https://toolforte.com/api/v1/tools/brp-test-data-generator \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key_here" \
  -d '{
    "format": "json",
    "options": {
      "count": 25,
      "seed": 20270101,
      "minAge": 60,
      "maxAge": 80,
      "referenceYear": 2027,
      "eventMix": { "married": 5, "widowed": 3, "nearRetirement": 4, "plain": 2 }
    }
  }'
· In a test, asserting on a fixed value
// The seed pins the data, so this assertion stays valid next month.
const res = await fetch(
  "https://toolforte.com/api/v1/tools/brp-test-data-generator",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.TOOLFORTE_KEY,
    },
    body: JSON.stringify({ options: { count: 10, seed: 42 } }),
  },
);
const { data } = await res.json();
expect(data.people[0].bsn).toBe(data.people[0].bsn); // same every run
expect(data.options.seed).toBe(42); // the run is reproducible from this
· A CSV with your own column names
{
  "format": "csv",
  "options": {
    "count": 100,
    "outputMode": "own",
    "headerRow": "burgerservicenummer;achternaam;geboortedatum",
    "delimiter": ";",
    "excelHint": true
  }
}

Set format to gba-totaalfile for the GBA totaalfile layout instead of a readable sheet.

Every option

Anything you leave out keeps the default below, which is the same value the tool page starts with. An option you misspell is refused rather than ignored, so you never get data that quietly differs from what you asked for.

Population
FieldType and rangeDefaultWhat it does
options.countinteger, 1 to 500050How many people to generate.
options.seednumber20270101The same seed always produces the same people. Fix it in a test suite so assertions stay valid; change it for a fresh set.
options.minAgeinteger, 0 to 1200Youngest age in the set, measured at the reference year.
options.maxAgeinteger, 0 to 12095Oldest age in the set. Must not be lower than minAge.
options.referenceYearinteger, 1900 to 20992027The year ages and life events are calculated against. Set it to the year your test scenario plays out in.
Life events
FieldType and rangeDefaultWhat it does
options.eventMixobject, keys: plain, married, registeredPartnership, divorced, widowed, remarried, deceased, deceasedWithFamily, minor, nearRetirement, livesAbroad, movedRecently, confidential, underInvestigation, noFixedAddress{"plain":6,"married":5,"registeredPartnership":2,"divorced":3,"widowed":2,"re…Relative weights per life event. Naming any event replaces the whole mix, so {married: 5, widowed: 2} gives you only those two. Omit the field entirely to keep the default spread. At least one weight must be above 0.
Family
FieldType and rangeDefaultWhat it does
options.includeParentstrue | falsetrueAdd parent records to each person.
options.maxChildreninteger, 0 to 102Upper bound on children per person.
Awkward data
FieldType and rangeDefaultWhat it does
options.pctExoticNamesinteger, 0 to 10015Percentage of unusual names: long, hyphenated, diacritics. Raise it to find where your system truncates or mangles input.
options.pctExoticStreetsinteger, 0 to 10015Percentage of unusual street names.
options.pctAffixinteger, 0 to 10025Percentage of surnames with a tussenvoegsel (van, de, ter). A classic source of sorting and matching bugs.
options.pctHouseAdditioninteger, 0 to 10020Percentage of addresses with a house number addition (12-A, 3bis).
Output
FieldType and rangeDefaultWhat it does
options.dateFormat"yyyymmdd" | "iso" | "dutch""yyyymmdd"Date notation: 20270131, 2027-01-31, or 31-01-2027. Match whatever your system reads.
options.outputMode"compact" | "own" | "mapped""compact"compact returns a readable overview. own returns exactly your own header row. mapped fills your columns from ours using the mapping below.
options.headerRowstring""Your own column names, separated by the delimiter. Required for outputMode own and mapped.
options.mappingobject of string to string{}For outputMode mapped: your column name to our field key. Columns you leave out come back empty.
options.delimiter"," | ";" | a tab","Column separator for the CSV formats and for headerRow.
options.excelHinttrue | falsefalsePrefix the CSV with a separator hint so Excel opens it in columns instead of one long string.

Life events you can weight

plainA person with only the basic record. The control group.
marriedA partnership of type H with a start date and a partner block.
registeredPartnershipA partnership of type P with a start date and a partner block.
divorcedA dissolved partnership with reason S in the previous partner block.
widowedA dissolved partnership with reason O, the partner died.
remarriedA current partnership plus an earlier dissolved one, so both blocks are filled.
deceasedA date, place and country of death on the person themselves.
deceasedWithFamilyDeath plus a surviving partner and a minor child, the classic survivor pension case.
minorUnder 18, for age rules and orphan scenarios.
nearRetirementAged 65 to 67.
livesAbroadAddress abroad in three free text lines with a country code, no Dutch postcode.
movedRecentlyA current address and a previous address, both dated.
confidentialConfidentiality indicator set, which many systems must respect in correspondence.
underInvestigationInvestigation start date filled, so the record is in onderzoek.
noFixedAddressRegistered with the municipality but without a residential address.
POST /api/v1/tools/upa-file-generator

UPA pension declarations.

Returns UPA XML for one or more consecutive periods, with employments, scheme blocks and household situations. Up to 10,000 employments per call.

· Three monthly declarations for 50 employments
curl -X POST https://toolforte.com/api/v1/tools/upa-file-generator \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key_here" \
  -d '{
    "format": "xml",
    "options": {
      "schemeType": "FPR",
      "population": { "count": 50, "seed": 20270101 },
      "period": { "startYear": 2027, "startMonth": 1, "months": 3 }
    }
  }'
· Files that should be rejected
{
  "format": "xml",
  "options": {
    "population": { "count": 10 },
    "defects": { "invalidBsn": true, "totalsMismatch": true }
  }
}

// The response warns you these are deliberate:
// "2 deliberate defects injected. These files are meant to be rejected."
· Only the file names, to keep a response small
{ "format": "summary", "options": { "population": { "count": 200 } } }

options.population

Population
FieldType and rangeDefaultWhat it does
population.countinteger, 1 to 1000025How many employments to generate.
population.seednumber20270101The same seed returns the same people, so a test can assert on a fixed BSN and a pipeline can rebuild its fixtures.
population.minAgeinteger, 15 to 7018Youngest employee.
population.maxAgeinteger, 15 to 8067Oldest employee. Must not be lower than minAge.
population.minSalaryinteger, 0 to 100000024000Lowest annual salary in euros.
population.maxSalaryinteger, 0 to 100000090000Highest annual salary in euros.
population.partTimeVariationtrue | falsetrueVary part-time percentages instead of putting everyone on 100 percent.
population.holidayAllowanceMonthinteger, 0 to 125Month the holiday allowance is paid. 0 means none.
Awkward data
FieldType and rangeDefaultWhat it does
population.pctForeignAddressinteger, 0 to 10012Percentage living abroad. Foreign addresses have no postcode in the Dutch format, which is where address validation tends to break.
population.pctExoticNamesinteger, 0 to 10015Percentage of unusual names.
population.pctExoticStreetsinteger, 0 to 10015Percentage of unusual street names.
population.longNamestrue | falsetrueInclude names long enough to hit column width limits.
population.hyphenAndApostrophetrue | falsetrueInclude names with hyphens and apostrophes.
population.diacriticstrue | falsetrueInclude accented characters, to catch encoding problems.
population.tussenvoegselstrue | falsetrueInclude Dutch name prefixes (van, de, ter).
population.houseNumberAdditionstrue | falsetrueInclude house number additions (12-A, 3bis).

options.period

Period
FieldType and rangeDefaultWhat it does
period.startYearinteger, 2000 to 20992027Year the first declaration covers.
period.startMonthinteger, 1 to 121Month the first declaration covers.
period.monthsinteger, 1 to 363How many consecutive periods to generate.
period.periodType"MND" | "VWK""MND"Monthly (MND) or four-weekly (VWK) periods.
period.declarationKind"volledig" | "aanvullend""volledig"A full declaration, or a supplementary one that assumes a full declaration for the period already exists.
period.splitMode"single" | "chunk" | "perParticipant""single"One file per period, files of a fixed size, or one file per participant. Use perParticipant when you need to trace a single case end to end.
period.chunkSizeinteger, 1 to 1000010Employments per file when splitMode is chunk.

options.household

Household
FieldType and rangeDefaultWhat it does
household.pctMarriedinteger, 0 to 10040Percentage married.
household.pctCohabitinginteger, 0 to 10015Percentage cohabiting.
household.pctDivorcedinteger, 0 to 10015Percentage divorced.
household.pctWidowedinteger, 0 to 1005Percentage widowed.
household.pctWithChildreninteger, 0 to 10045Percentage with children.
household.partnerSameFundtrue | falsetrueAllow partners who are in the same pension fund.
household.partnerOtherFundtrue | falsetrueAllow partners in a different fund.
household.deceasedPartnerstrue | falsetrueInclude deceased partners.
household.deceasedExPartnerstrue | falsetrueInclude deceased ex-partners.
household.deceasedChildrentrue | falsefalseInclude deceased children.

Scenarios and defects

options.scenarioMix takes a weight per scenario, and options.defects takes a boolean per defect.

standardUnchanged employment for the whole run. The control group.
newHireStarts inside the run. DatAanv falls in the period it first appears.
leaverDatEind plus CdRdnEindArbov in the period the employment ends.
rehireLeaves, then returns with a NEW NumIV, as the IKV rules require.
payrollSwitchKeeps the same NumIV across the switch, as the IKV rules require.
deathInServiceDatOvl filled and DatEind set to the same date.
marriageCdBurgSt goes to 2 with a DtIngBurgSt in the period the marriage takes effect.
registeredPartnershipCdBurgSt goes to 5 with a DtIngBurgSt in the period the partnership starts.
divorceCdBurgSt goes to 3 with a DtIngBurgSt in the period the divorce is registered.
widowedCdBurgSt goes to 4 with DtIngBurgSt.
unpaidLeaveCdIncInkVerm O plus the Verlof group with CdSrtVerl OBD, and VerlofUReg from UPA 2025 onwards.
parentalLeaveCdIncInkVerm K (kindverlof) plus the Verlof group with CdSrtVerl OSP. Code G was replaced by K in UPA 2022.
sicknessCdIncInkVerm Z with a reduced RegLn.
earlyIvaCdIncInkVerm Z with RegLn 0 and AantVerlUPens 0.
onCallIndOprov J, AantUCntrWk 0, and a period with RegLn 0.
internCdAard 7 and no RegelingGegevens at all.
dgaSrtIV 17 and no CdAard, so no scheme participation.
minorUnder 18 with a small salary, for entry-age rules.
nearRetirementAged 65 to 67, for retirement and AOW triggers.
partTimeChangePtPerc and AantVerlUPens change halfway through the run.
backPaymentA pensionable payment in a period after DatEind.
invalidBsnOne employment gets a 9-digit BSN that does not pass the 11-check.
duplicateBsnTwo employments in one file carry the same BSN.
duplicateNumIvTwo employments in one file carry the same income relation number.
totalsMismatchTotRegLn and TotPremieReg are off by 10 percent against the detail lines.
endBeforeStartDatEindRegGeg is set before DatAanvRegGeg on one employment.
dateOutsidePeriodOne DatAanvRegGeg falls a month before the period start.
unknownRegVrntOne employment uses a RegVrnt that does not exist in the scheme.
negativeAmountOne RegLn and PremieGrslg are negative.
zeroNormHoursAantUNormWk is 0 while scheme data is supplied, which the spec rejects.
missingMandatorySofiNr is left out of one employment.

The remaining sections

declaration, schemes, fieldStates and pinned take exactly the shape the tool page uses. Rather than describe them in prose, here is the complete default request. Send back any part of it with your own values.

· Complete default options
{
  "schemaVersion": "2024-01",
  "schemeType": "FPR",
  "declaration": {
    "employerName": "TESTWERKGEVER ALFA BV",
    "payrollTaxNumber": "100000015L01",
    "contactPerson": "MW A. TESTBEHEERDER",
    "phone": "06-10000001",
    "relationNumber": "SWO00077",
    "softwarePackage": "LOKET.NL",
    "providerId": "PUO000001",
    "administrationId": "UPA000086",
    "messageIdPrefix": "TEST",
    "affiliationNumber": "18018",
    "collectiveAgreement": "316",
    "sector": "17",
    "taxTable": "014",
    "jobDescription": "ALGEMEEN",
    "normHoursPerWeek": "40",
    "franchise": 18000,
    "maxPensionableSalary": 80000
  },
  "schemes": [
    {
      "enabled": true,
      "label": "Basisregeling",
      "schemeCode": "U0574-3001",
      "variants": "FPR00, FPR01, FPR02, FPR03",
      "premiumPct": 27.6,
      "useFranchise": true
    },
    {
      "enabled": false,
      "label": "Aanvullende regeling",
      "schemeCode": "U0574-3002",
      "variants": "FPR90",
      "premiumPct": 4.2,
      "useFranchise": true
    },
    {
      "enabled": false,
      "label": "Sociaal fonds",
      "schemeCode": "U0574-9001",
      "variants": "SF001",
      "premiumPct": 0.6,
      "useFranchise": false
    }
  ],
  "fieldStates": {
    "ContPers": "fill",
    "TelNr": "fill",
    "RelNr": "fill",
    "GebrSwPakket": "fill",
    "IdLcr": "fill",
    "NmLcr": "fill",
    "NmIP": "fill",
    "Voorl": "fill",
    "Voorv": "fill",
    "Nat": "fill",
    "Gesl": "fill",
    "DatOvl": "fill",
    "CdBurgSt": "fill",
    "DtIngBurgSt": "fill",
    "TaalVoorkeur": "omit",
    "Email": "omit",
    "Adres": "fill",
    "HuisNrToev": "fill",
    "LocOms": "omit",
    "DatEind": "fill",
    "CdRdnEindArbov": "fill",
    "CdAard": "fill",
    "CdInvlVpl": "omit",
    "CAO": "fill",
    "IndArbovOnbepTd": "fill",
    "IndOprov": "fill",
    "LbTab": "fill",
    "CdBeroep": "omit",
    "FuncOms": "fill",
    "CdIncInkVerm": "fill",
    "AantUNormWk": "fill",
    "AantUCntrWk": "fill",
    "PtPerc": "fill",
    "CdVerbIkv": "omit",
    "IndGenReg": "fill",
    "CollectieveAangifte": "fill",
    "Werknemersgegevens": "fill",
    "Avwb": "fill",
    "Dgln": "omit",
    "Verlof": "fill",
    "Sector": "fill",
    "DatEindSect": "omit",
    "IndEindDln": "fill",
    "PremieWkn": "fill",
    "AantVerlUPens": "fill",
    "VerlofUReg": "fill"
  },
  "pinned": []
}

From an AI assistant.

Both generators are on the MCP server as generate_brp_test_data and generate_upa_files. Counts are capped lower there than over REST, because an assistant pays for every token it reads back. For bulk work, have it write a script against the REST endpoint instead.

· What you can ask an assistant
"Generate 10 Dutch test people over 60, half of them widowed,
 and put them in a CSV I can import."

"Make a UPA declaration for 20 employments over 3 months in 2027,
 and include a file with a mismatched total so I can test the rejection."

This page is generated from the same parameter definitions the API validates against, so it cannot fall behind the code.