Voting tracker: how did each party vote?
Search a topic and see how each party in the House of Representatives voted on the motions about it: how often for, how often against, and which motions passed.
geef_project("stemgedrag-tracker")geef_bron("tweedekamer-opendata")
I have the project, the tested calls and the pitfalls. I'll build a app with Dutch House of Representatives open data. This is how it could look:
You build this yourself. Copy the prompt into Claude, ChatGPT or Cursor, with the MCP server on, or follow the steps below.
Motion by member Teunissen on including the call for a ‘More Plant-Based Food’ action plan in the further elaboration of the nitrogen package and the food strategy
- D6626
- PRO20
- CDA18
- SP3
- PvdD3
- ChristenUnie3
- DENK3
- 50PLUS2
- Volt1
- VVD22
- PVV19
- JA219
- FVD7
- Groep Markuszower7
- BBB3
- SGP3
- Keijzer1
This is a mockup, not a working version, with the vote on motion 2026Z19131 from a call on 17 October 2026, 23:33 (House of Representatives, Gegevensmagazijn) and the totals from our test run on 16 October 2026. Turn the MCP server on with the address on For AI.
Who it's for
Voters who want to know how a party actually votes, journalists and fact-checkers, school and politics students, and makers of voting advice tools.
What you get
A search page: you type a topic and get, per party, how many motions on that topic it supported and how many not, plus the list of motions and their outcome.
Test run on 16 October 2026 for 'stikstof' (nitrogen) in the House that started on 12 November 2025: 28 motions, 26 put to a vote and 8 adopted. BBB and ChristenUnie voted for 18 of the 26 (69%), VVD, PvdD and DENK for 6 (23%). For 'asiel' (asylum) there were 78 motions (72 voted on, 25 adopted); FVD voted for 90%, PVV 89%.
Motions filed in the House: 4,440 per year in 2025, 70% more than in 2009
Motions filed in the House, 2009 – 2025, per year. The highest point was 2022 (5,010).
How you could start5 steps
- 1
Which parties sit in the House now?
Parties with DatumInactief empty are the current ones. Together 150 seats in 17 parties on 16 October 2026, including splinter groups such as Groep Markuszower and the one-member group Keijzer.
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Fractie?$filter=Verwijderd eq false and DatumInactief eq null&$select=Afkorting,NaamNL,AantalZetels&$orderby=AantalZetels desc&$top=5
{"@odata.context":"https://gegevensmagazijn.tweedekamer.nl/OData/v5/$metadata#Fractie(Afkorting,NaamNL,AantalZetels)","value":[ {"Afkorting":"D66","NaamNL":"Democraten 66","AantalZetels":26}, {"Afkorting":"VVD","NaamNL":"Volkspartij voor Vrijheid en Democratie","AantalZetels":22}, {"Afkorting":"PRO","NaamNL":"Progressief Nederland","AantalZetels":20}, {"Afkorting":"PVV","NaamNL":"Partij voor de Vrijheid","AantalZetels":19}, {"Afkorting":"CDA","NaamNL":"Christen-Democratisch Appèl","AantalZetels":18}]} - 2
Search motions by subject, with the votes included
contains(Onderwerp,'stikstof') searches the subject line, case-insensitive, and also finds 'stikstofbeleid'. With $expand you get the decision and the vote per party in one call: Soort (Voor, Tegen, Niet deelgenomen) and FractieGrootte (seats). $count=true gives the total (28).
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Zaak?$filter=Soort eq 'Motie' and Verwijderd eq false and contains(Onderwerp,'stikstof') and Datum ge 2025-11-12&$count=true&$top=1&$orderby=Datum desc&$select=Nummer,Onderwerp,Datum&$expand=Besluit($select=Soort;$filter=Verwijderd eq false;$expand=Stemming($select=Soort,FractieGrootte,FractieNaam;$filter=Verwijderd eq false))
{"@odata.context":"…/$metadata#Zaak(Nummer,Onderwerp,Datum,Besluit(Soort,Stemming(Soort,FractieGrootte,FractieNaam)))","@odata.count":28,"value":[ {"Nummer":"2026Z20455","Onderwerp":"Motie van het lid Van der Plas over de maatschappelijke en sociaal-economische gevolgen voor de landbouw en het platteland expliciet meewegen bij de uitwerking van het stikstofbeleid ","Datum":"2026-09-29T00:00:00+02:00", "Besluit":[{"Soort":"Stemmen - verworpen","Stemming":[ {"Soort":"Tegen","FractieGrootte":26,"FractieNaam":"D66"},{"Soort":"Tegen","FractieGrootte":18,"FractieNaam":"CDA"}, {"Soort":"Voor","FractieGrootte":3,"FractieNaam":"BBB"},{"Soort":"Tegen","FractieGrootte":3,"FractieNaam":"DENK"}, … - 3
Watch out for roll-call votes
Usually a party votes as a whole: one row with FractieGrootte. In a roll-call vote there is one row per member, with Persoon_Id and Naam, and every member still carries the full FractieGrootte. Count 1 per person then, otherwise you count a party twenty times with twenty seats.
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Stemming?$filter=Verwijderd eq false and Persoon_Id ne null&$orderby=GewijzigdOp desc&$top=2&$select=Soort,Naam,FractieNaam,FractieGrootte,Persoon_Id,Besluit_Id
{"@odata.context":"…/$metadata#Stemming(Soort,Naam,FractieNaam,FractieGrootte,Persoon_Id,Besluit_Id)","value":[ {"Besluit_Id":"f15d2d9f-85ed-42df-baa1-33706bd5f7f0","Soort":"Voor","FractieGrootte":1,"Naam":"Keijzer, M.C.G.","FractieNaam":"Keijzer","Persoon_Id":"8c5a8186-8f46-4bca-956d-06d4fa521635"}, {"Besluit_Id":"f15d2d9f-85ed-42df-baa1-33706bd5f7f0","Soort":"Tegen","FractieGrootte":20,"Naam":"Mohandis, M.","FractieNaam":"PRO","Persoon_Id":"b7a52679-bac9-4e00-9dfd-06a22a7cf088"}]} - 4
A party that changes its name
GroenLinks-PvdA has been called PRO (Progressief Nederland) since 9 June 2026 and got a new Fractie_Id. This year's votes contain both names. Look up successors with DatumActief and DatumInactief and merge them, otherwise it looks like two parties.
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Fractie?$filter=Afkorting eq 'PRO' or Afkorting eq 'GroenLinks-PvdA'&$select=Id,Afkorting,NaamNL,DatumActief,DatumInactief
{"@odata.context":"…/$metadata#Fractie(Id,Afkorting,NaamNL,DatumActief,DatumInactief)","value":[ {"Id":"d60f373a-5bb2-477f-9979-a8f3a0514e6b","Afkorting":"PRO","NaamNL":"Progressief Nederland","DatumActief":"2026-06-09T00:00:00+02:00","DatumInactief":null}, {"Id":"0208097d-ef04-438a-8c29-eebb84956204","Afkorting":"GroenLinks-PvdA","NaamNL":"GroenLinks-PvdA","DatumActief":"2023-10-27T00:00:00+02:00","DatumInactief":"2026-06-10T00:00:00+02:00"}]} - 5
Count per year in the API itself
With $apply you count without fetching everything. Put the filter inside $apply (filter(…)/groupby(…)); a separate $filter next to a groupby returned an error. Nitrogen motions: 91 in 2022, 26 in 2026 so far.
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Zaak?$apply=filter(Soort eq 'Motie' and Verwijderd eq false and contains(Onderwerp,'stikstof'))/compute(year(Datum) as Jaar)/groupby((Jaar),aggregate($count as n))
{"@odata.context":"…/$metadata#Zaak(Jaar,n,Jaar)","value":[ {"@odata.id":null,"Jaar":2008,"n":2}, … {"@odata.id":null,"Jaar":2019,"n":34},{"@odata.id":null,"Jaar":2020,"n":27}, {"@odata.id":null,"Jaar":2021,"n":18},{"@odata.id":null,"Jaar":2022,"n":91},{"@odata.id":null,"Jaar":2023,"n":52}, {"@odata.id":null,"Jaar":2024,"n":23},{"@odata.id":null,"Jaar":2025,"n":19},{"@odata.id":null,"Jaar":2026,"n":26}]}
Let AI build it
Paste this prompt into Claude, ChatGPT, Cursor or Lovable. It names the real endpoints, the join keys and the pitfalls, so the assistant writes working code straight away. Always check the result against the source itself.
Build a web app (a single HTML file with JavaScript, or Next.js) where I type a topic and see how each party in the House of Representatives voted on motions about it.
API: House of Representatives Gegevensmagazijn, OData v5, no key, CORS open:
https://gegevensmagazijn.tweedekamer.nl/OData/v5/Zaak?$filter=Soort eq 'Motie' and Verwijderd eq false and contains(Onderwerp,'stikstof') and Datum ge 2025-11-12&$select=Nummer,Onderwerp,Datum&$expand=Besluit($select=Soort;$filter=Verwijderd eq false;$expand=Stemming($select=Soort,FractieGrootte,FractieNaam,Persoon_Id;$filter=Verwijderd eq false))&$orderby=Datum&$top=250
What you need to know:
- Use v5 field names. Old v4 names (GestartOp, ActorFractie, ActorNaam) return an error: in v5 they are Datum, FractieNaam and Naam.
- Always filter on Verwijderd eq false (also inside $expand).
- $top at most 250; follow @odata.nextLink for the next page.
- contains() is case-insensitive and only searches the motion's subject line.
- Outcome: the decision with Soort 'Stemmen - aangenomen' or 'Stemmen - verworpen' and a filled Stemming list. Other decisions ('Ingediend', 'Overgenomen', 'Stemmen - aangehouden') have no votes. Decisions are not in order.
- Stemming.Soort is Voor, Tegen or Niet deelgenomen. In a party vote there is one row per party with FractieGrootte (seats). In a roll-call vote one row per member (Persoon_Id filled): count 1 per person and take the majority per party.
- Parties change: GroenLinks-PvdA has been called PRO since 9 June 2026. Fetch /Fractie (Afkorting, NaamNL, AantalZetels, DatumActief, DatumInactief) and merge predecessors and successors.
- The current House started on 12 November 2025. For earlier Houses, compare per term: parties and seat counts differ.
- The Vergissing field (true for 1,437 of over 1 million vote rows) marks a vote reported as a mistake. Keep the vote and show a footnote.
Output: a search box; a table per party (seats, number for, number against, percentage for), sorted by percentage; below it the motions with date, subject, outcome and a bar with for and against votes in seats. Optional: pick two parties and see how often they voted the same.
Try it yourself
The core of the project in a small script. Needs: Node 20 of nieuwer (ingebouwde fetch); draai met: node stemgedrag.mjs [woord] [vanaf-datum]. Tested on 2026-10-16; below is the real output of that run.
// Stemgedrag-tracker: hoe stemde elke fractie over moties over een onderwerp?
// Bron: Tweede Kamer Gegevensmagazijn, OData v5 (geen sleutel). Node 20+.
const UA = { "User-Agent": "mijn-project/1.0 (+jij@voorbeeld.nl)" };
const TK = "https://gegevensmagazijn.tweedekamer.nl/OData/v5";
const WOORD = process.argv[2] ?? "stikstof";
const VANAF = process.argv[3] ?? "2025-11-12"; // Kamer na de verkiezing van 29 oktober 2025
// Moties met het woord in het onderwerp, met besluiten en per besluit de stemmingen per fractie.
let url = `${TK}/Zaak?` + new URLSearchParams({
$filter: `Soort eq 'Motie' and Verwijderd eq false and contains(Onderwerp,'${WOORD}') and Datum ge ${VANAF}`,
$select: "Nummer,Onderwerp,Datum",
$expand: "Besluit($select=Soort;$filter=Verwijderd eq false;$expand=Stemming($select=Soort,FractieGrootte,FractieNaam,Persoon_Id,Vergissing;$filter=Verwijderd eq false))",
$orderby: "Datum", $top: "250", // meer dan 250 geeft een fout; volg @odata.nextLink
});
const moties = [];
while (url) { const j = await (await fetch(url, { headers: UA })).json(); moties.push(...j.value); url = j["@odata.nextLink"]; }
// Fracties veranderen van naam en krijgen dan een nieuwe Fractie_Id: GroenLinks-PvdA heet sinds 9 juni 2026 PRO
// (Progressief Nederland). Voeg ze hier samen; zoek andere gevallen op met /Fractie (DatumActief, DatumInactief).
const ZELFDE = { "GroenLinks-PvdA": "PRO" };
const perFractie = {}; // fractie -> { voor, tegen }
let gestemd = 0, aangenomen = 0;
for (const m of moties) {
// Het besluit met de stemming: Soort 'Stemmen - aangenomen' of 'Stemmen - verworpen' en een gevulde Stemming-lijst.
const b = m.Besluit.find((b) => /^Stemmen - (aangenomen|verworpen)/.test(b.Soort) && b.Stemming.length);
if (!b) continue;
gestemd++; if (b.Soort.includes("aangenomen")) aangenomen++;
// Hoofdelijke stemming: één rij per Kamerlid (Persoon_Id gevuld). Tel dan per fractie de meerderheid.
const stem = {};
for (const s of b.Stemming) {
if (s.Soort === "Niet deelgenomen") continue;
const f = (stem[ZELFDE[s.FractieNaam] ?? s.FractieNaam] ??= { Voor: 0, Tegen: 0 });
f[s.Soort] += s.Persoon_Id ? 1 : s.FractieGrootte;
}
for (const [f, t] of Object.entries(stem)) {
const p = (perFractie[f] ??= { voor: 0, tegen: 0 });
t.Voor >= t.Tegen ? p.voor++ : p.tegen++;
}
}
console.log(`Moties met '${WOORD}' in het onderwerp sinds ${VANAF}: ${moties.length}, waarvan ${gestemd} in stemming (${aangenomen} aangenomen)`);
console.log("fractie voor tegen % voor");
for (const [f, p] of Object.entries(perFractie).sort((a, b) => b[1].voor / (b[1].voor + b[1].tegen) - a[1].voor / (a[1].voor + a[1].tegen)))
console.log(` ${f.padEnd(18)} ${String(p.voor).padStart(4)} ${String(p.tegen).padStart(5)} ${Math.round((100 * p.voor) / (p.voor + p.tegen)).toString().padStart(5)}`);
const laatste = moties.filter((m) => m.Besluit.some((b) => b.Stemming.length)).at(-1);
if (laatste) console.log(`\nLaatste: ${laatste.Nummer} (${laatste.Datum.slice(0, 10)}) ${laatste.Onderwerp.trim()}`);
Moties met 'stikstof' in het onderwerp sinds 2025-11-12: 28, waarvan 26 in stemming (8 aangenomen) fractie voor tegen % voor BBB 18 8 69 ChristenUnie 18 8 69 SGP 17 9 65 Groep Markuszower 15 9 63 JA21 16 10 62 PVV 16 10 62 FVD 16 10 62 Keijzer 13 9 59 50PLUS 12 14 46 SP 12 14 46 PRO 10 16 38 Volt 9 17 35 D66 8 18 31 CDA 8 18 31 PvdD 6 20 23 VVD 6 20 23 DENK 6 20 23 Laatste: 2026Z20455 (2026-09-29) Motie van het lid Van der Plas over de maatschappelijke en sociaal-economische gevolgen voor de landbouw en het platteland expliciet meewegen bij de uitwerking van het stikstofbeleid
Where it goes wrong
- Roll-call votes give one row per member, and each member carries the full FractieGrootte. Summing FractieGrootte counts a twenty-seat party twenty times. Count 1 per person when Persoon_Id is filled.
- Parties change name and Id (GroenLinks-PvdA became PRO on 9 June 2026) and split (Groep Markuszower, Keijzer). Without merging, PRO appears to vote on only part of the 2026 motions.
- A $filter next to an $apply with groupby returns an error ('grouping expression must evaluate to a property access value'). Put the filter inside $apply: $apply=filter(…)/groupby(…).
- Not every motion is voted on: of the 28 nitrogen motions 26 were put to a vote; the rest were taken over, postponed or withdrawn. Count those separately and only compute percentages over motions with a vote.
- Searching a word in the subject misses motions phrased differently ('Natura 2000', 'PAS-melders'). Let users enter several words and show which motions you found, so they can check.
- Use v5 (/OData/v5). The old v4 address was due to stop from 1 October 2026 and has different field names; an old name in v5 returns 'Could not find a property named …'.
Take it further
- Make an agreement matrix: for each pair of parties the percentage of motions on which they voted the same. That shows which parties work together in practice.
- Add the sponsors: ZaakActor (Rol 'Indiener', with Naam and FractieNaam) tells who filed the motion. How often does a party vote against its own kind of motions?
- Follow a topic over time: with $apply and groupby per year you see when it was on the agenda (nitrogen: 91 motions in 2022).
- Send a notification on new votes: every day request Besluit with GewijzigdOp ge <yesterday> and Soort 'Stemmen - …' and look up the related motions.
More with this data
Seats in the House: D66 is the largest part, 17% in oktober 2026
Breakdown in oktober 2026: D66 17%, VVD 15%, PRO 13%, PVV 13%, CDA 12%, JA21 6%, Groep Markuszower 4.7%, other (10) 19%. Total: 150 seats.
The sources1

Dutch House of Representatives open data
Everything the House does, as an open API: motions and amendments, how each party voted, written questions, debates and committees, parliamentary papers, and the members themselves, with their side jobs, gifts and trips.
Municipal budget explorer: what does my municipality spend money on?
Council decision alert: a ping when your municipality decides something
Election map: result, shift and turnout per municipality