--- name: riigikogu-open-data description: Riigikogu API: trace a bill end to end, plus votes, MPs, committees, agendas, stenograms, documents. module: riigikogu.mjs execution: get --- # Riigikogu Open Data API ## Vetted module (use this first) `riigikogu.mjs` ships in https://kodanikukratt.ee/kratt-agent.tar.gz (plain ESM, Node 22+) and is re-verified against the live source. Call it instead of hand-writing these requests. Everything it does is also written out below as ordinary HTTPS requests, so this recipe stands alone without it. ```js // The whole life of one bill: initiators, opinions, committee decisions, readings, votes, texts. const t = await draftTrace("418 SE"); show({ title: t.title, stage: t.stage, initiators: t.initiators, counts: t.counts, page: t.page }); show(t.opinions.map((o) => ({ date: o.date, title: o.title }))); // One committee's sittings, agenda items and invited officials. const c = await committeeSittings("julgeolekuasutuste järelevalve erikomisjon", { from: "2026-01-01", to: "2026-06-30" }); show({ committee: c.committee, totals: c.totals, source: c.source }); // counts from `totals`, not from printed rows // How one MP voted, and every plenary vote in a window with tallies. const r = await memberVotings("Lauri Hussar", { from: "2026-05-01", to: "2026-07-24" }); show({ member: r.member, votes: r.votings.length, truncated: r.truncated, page: r.page }); show(await sittingVotings({ from: "2026-06-01", to: "2026-06-30" }).then((v) => [v.totals, v.sessionNote, v.source])); ``` - `draftTrace(markOrUuid, {membership, draftTypeCode})`: `"418 SE"` → uuid, then the 46 KB draft detail flattened to ~14 KB. `introduction` is the seletuskiri's own purpose statement; `readings[].events[]` carries committee decisions, plenary statuses, vote links and protocol PDFs. - `memberVotings`: pages that member's own votes, presence checks (`KOHALOLEKU_KONTROLL`) dropped, newest first. `truncated: true` means older votings were omitted — say so. - `sittingVotings`: one entry per sitting with `inFavor`/`against`/`neutral`/`abstained` per voting. - `totals`/`counts` are computed over the whole result set, so they stay right when you print only a slice (observed failure: answering "13 sittings, 81 votings" against data holding 15 and 106). State every number from them, never from the rows you printed. - Also exported: `findMember(name)`, `plenaryMembers()`, `findUsergroup(name)`, `draftCount(filters)` (see "Bill statistics"). - All retry HTTP 429 from one shared budget, so a rate-limited run degrades to a partial answer rather than burning the step cap. The endpoints below are the fallback for what the module does not cover, or if a call throws. ## Avoid when - You need election outcomes (use election archive skill). - You need what was *said* in a committee — committee verbatim does not exist. The protocol PDF is the record; see "Committees" below. - You need the ministry drafting stage or the government's own decision — those are outside this API (ministry document registers, Riigikantselei). ## Primary endpoints - OpenAPI contract: https://api.riigikogu.ee/v3/api-docs (72 paths, 181 KB — too large to read whole) - Draft search and draft detail: `…/api/volumes/drafts?…` and `…/api/volumes/drafts/{uuid}?lang=ET&querySteno=false` — see the two sections on bills below - Votes in a window, tallies inline: `https://api.riigikogu.ee/api/votings?startDate=2025-01-01&endDate=2025-01-31&lang=ET` - One voting, with per-member votes: `https://api.riigikogu.ee/api/votings/{uuid}?lang=ET` - One member's votes (with each decision inline): `https://api.riigikogu.ee/api/votings/plenary-member/{memberUuid}?startDate=2026-05-01&endDate=2026-06-30&lang=ET&size=100&page=0` - One member's committee votes: `https://api.riigikogu.ee/api/votings/committees?userUuid={memberUuid}&startDate=2026-01-01&endDate=2026-06-30&lang=ET` - Plenary agendas: `https://api.riigikogu.ee/api/agenda/plenary?startDate=2025-01-01&endDate=2025-01-31&lang=ET` - Documents, stenograms and 1000-signature petitions: see their own sections below - Proceeding-stage codes: `https://api.riigikogu.ee/api/classifiers/menetlusetapid` - Public fallbacks: https://www.riigikogu.ee/tegevus/kalender/ and https://www.riigikogu.ee/tegevus/stenogrammid/ ## Members: resolve the UUID from the name first Every per-member endpoint is keyed by UUID, and a member UUID is NEVER guessable — a fabricated one returns an empty page that looks like "this MP has no votes". Ask `/api/plenary-members` for the name first and read the `uuid` out of the response: - `https://api.riigikogu.ee/api/plenary-members?name=Kaljulaid&lang=ET` → one record, ~3 KB, with `uuid`, `factions`, `committees`, electoral district. - `name` matches a case-insensitive substring of the first OR last name, so a surname alone is enough; a full name (`name=Raimond Kaljulaid`) also works. Letters, spaces, hyphens and apostrophes only — other characters return HTTP 400. - Default is sitting members only. For someone who has left, add `status=ALL` (or `status=INACTIVE`); values are uppercase. - **Never request `/api/plenary-members` without `name=`**: the full list of 101 members is ~440 KB and gets cut at the 20 000-character response limit, i.e. about four members, alphabetically from "Aab". A single `name=` query is also the cheaper request. Then feed that `uuid` into `/api/votings/plenary-member/{uuid}`, `/api/votings/committees?userUuid={uuid}`, `/api/statistics/votings/member/{uuid}`, or `/api/plenary-members/{uuid}`. ## Committees: same rule, plus `hideInactive=true` Committees, erikomisjonid, factions and delegations are all "usergroups", keyed by UUID and equally unguessable: - `https://api.riigikogu.ee/api/usergroups?name=julgeolek&hideInactive=true&lang=ET` → the one active match, ~0.4 KB. `name` is a case-insensitive substring and accepts spaces and Estonian letters. - **Always pass `hideInactive=true`.** The register keeps every unit since 1992: `?typeCode=ERIKOMISJON` alone returns 40 rows, 36 of them dissolved, several near-identical in name. An agenda query against a dissolved committee returns an empty list that reads as "it never met". - `?typeCode=ALALINE_KOMISJON&hideInactive=true` → the 11 standing committees; `?typeCode=ERIKOMISJON&hideInactive=true` → the 4 special/inquiry committees. Other codes: `FRAKTSIOON`, `DELEGATSIOON`, `PARLAMENDIRYHM`, `UURIMISKOMISJON`. - Never request `/api/usergroups` bare: 347 rows, 98 KB, over the response limit. Then `/api/agenda/usergroup/{uuid}?startDate=…&endDate=…` → `{weekStartDate, weekEndDate, sittings[]}` (the `week*` names are wrong: the window is whatever you asked for). Each sitting has `sittingDateTime`, `sittingTypeCode` (`KORRALINE`/`ERAKORRALINE`/`UHISISTUNG`), `agendaItems[]` (`title`, and `invitees` as free text where officials were summoned) and `protocolDocuments[]` with the files. For "which committees met at all" across a date range rather than one named committee, `/api/events?startDate=…&endDate=…&type=COMMITTEE&lang=ET` lists every committee's sittings per day — but 5 days is 144 KB, so prefer the single-committee route whenever the question names one. **Committee verbatim does not exist**: `?type=IK` on `/api/steno/verbatims` is silently ignored and returns the plenary transcript for those dates instead. What was argued in a committee is only in the protocol PDF — not word-for-word, but with named speakers, each amendment and whether it was voted. Read it with `pdfText(download)` from `pdf.mjs`. **Attendance opens every protocol** (format stable 2024–2026 across committees): `Juhataja:`, `Protokollija:`, then `Võtsid osa:` with `Komisjoni liikmed:` and `Komisjoni ametnikud:`, then `Puudusid:` and `Kutsutud:` (guests, tagged per agenda item). Read whole labelled blocks — names wrap across lines, the last is joined with "ja", and the chair is NOT repeated under `Komisjoni liikmed:`. A missing `Puudusid:` line means nobody was recorded absent. One protocol is one sitting: counting attendance over a period means reading every protocol in it — enumerate via `committeeSittings()` → `protocols[].download`, not document title search (registered titles are inconsistent: "PÕSK protokoll nr 96"), and report a sitting with no protocol as a gap, not an absence. ## Trace one bill: what lives where `GET /api/volumes/drafts/{uuid}` is the whole story in one 30–50 KB response, and it is the same 28 top-level keys on every bill — do not go hunting. The table maps the ones that answer questions; the rest are `copyOrOriginal`, `membership`, `titleWithMarkAndTypeCode`, `initialTitle`, `activeDraftStatus`, `initiated`, `joined`, `accepted`, `amendmentsDeadline`, `responsibleOfficer`, `descriptors[]`, `relatedVolumes[]`, `preProcedureDocuments[]`, `preProcedureVolumes[]`, `_links`. | Question | Where | |---|---| | Who initiated it, and why | `initiators[].name` (an MP, a faction, a committee, or the classifier `Vabariigi Valitsus`) + `introduction` | | Which committee leads it | `leadingCommittee.name`, `responsibleMember.name` | | Who wrote in (associations, ministries, law firms, WHO…) | `opinions[]` — `{date, title, files[]}`, one per submission; the sender is in the title | | Proposed changes to the text | `amendments[]` — `{title, files[]}` (`documentType: "muudatusettepanek"`) | | What happened when, and what each committee decided | `readings[]` — `readingCode` is `INITIATION`, `FIRST_READING`, `SECOND_READING`, `THIRD_READING`, `EFFECTUATION` → `proceedingEvents[]` with `date`, `sittingTitle`, `status`/`decisionCode`, `committeeDecisions[]`, `votings[]`, `protocolFiles[]` (the committee protocol PDF, already here — no document search needed) | | How the plenary voted | `readings[].proceedingEvents[].votings[]` gives the voting uuid only — follow `/api/votings/{uuid}` for `inFavor`/`against`/`abstained`/`present` | | Signature and publication | the `EFFECTUATION` reading: statuses `SAADETUD_VABARIIGI_PRESIDENDILE`, `VALJAKUULUTATUD`, `AVALDATUD_RIIGITEATAJAS` | | The bill text, seletuskiri, impact analyses at each stage | `texts[]` — `{readingCode, document:{documentType}, file:{fileName, _links.download.href}}`. `documentType` is `algtekst`, `lugemiseTekst`, `lopptekst`, `lisadokumendid` or `vabariigiPresidendiOtsus`; the **seletuskiri is a file inside those**, named like `Seletuskiri_.pdf`, not a documentType of its own | | Whether it became law | `activeDraftStage`: `VASTU_VOETUD` passed, `TAGASI_LYKATUD` rejected, `TAGASI_VOETUD` withdrawn, `VALJA_LANGENUD_KOOSEISU_LOPPEMISEGA` lapsed with the composition | **Mark → uuid**: `?mark=418&draftTypeCode=SE` returns **8 bills** — a mark is reused every Riigikogu composition. Add `membership=15` for the current one (`/api/memberships/current` gives the number), or pick the record with the highest `membership`. `draftTrace()` does this for you. ## Bill statistics: cohorts, proposers, and what "passed" means `draftStats({from, to, byProposer})` is the entry point for any "how many bills / what share passed / by whom / by month" question. Two requests for the government split; ~28 with `byProposer: true`. **Read the `note` and `proposerNote` it returns**, which restate the two rules below against the cohort you actually asked for. ```js const s = await draftStats({ from: "2025-01-01", to: "2025-12-31", byProposer: true }); show({ asOf: s.asOf, cohortTotal: s.cohortTotal, totals: s.totals, proposers: s.proposers, page: s.page }); show([s.note, s.proposerNote, s.stages]); show(s.months.map((m) => [m.month, m.initiated, m.passed, m.pending, m.passedOfInitiated, m.passedOfDecided])); ``` - **Two rates, and neither one is "eventually passed".** The register knows whether a bill HAS passed, never whether a pending one will. `passedOfInitiated` keeps bills still in proceedings in the denominator; `passedOfDecided` counts only terminal outcomes (`pending` is `proceedingStatus: "IN_PROCESS"`, already in the search response). 2025: 106 of 175 = **60.6% of initiated, 65.4% of the 162 decided**, 13 still at second reading (`stages`). Say which denominator you used. - **A cohort is by INITIATION month**, outcome as of `asOf`. A bill initiated 2025-12-18 and passed 2026-05-20 counts in **December** — 44 of the 106 bills initiated in 2025 reached their current stage in a later year, and every December one did. "Share of December's bills that passed" and "bills passed in December" are different questions; name which you answered. A month with no bills initiated has a **`null` rate, not 0%**. - **Proposer categories are mutually exclusive**, by precedence: government > faction > committee > members. Factions and committees are BOTH `type: "usergroup"`, and 12 of the 2025 bills name a faction *together with* individual MPs — counted once, under the faction, or the categories stop summing to the cohort. 2025: 103 government (96 passed), 55 faction (2), 4 committee (4), 13 MPs (4). - **The draft SEARCH response carries no `initiators[]`** — only `/api/volumes/drafts/{uuid}` does. Do not fetch one detail record per bill: subtract `initiatorUuid` sets first, then ask only about what is left (28 requests for 2025 against 176, identical bill for bill, verified 2026-08-02). Pace it — see "Request contract". - One number on its own: `draftCount({from, to, initiatorUuid, stage})` → `page.totalElements` in one request. **Vabariigi Valitsus** is `initiatorUuid=65e1db80-49f1-4c5b-b1b6-539b7efd199f`; other initiator UUIDs from `findUsergroup()` / `findMember()`, or `/api/lists/draft-initiators` (744 entries: 626 MPs, 113 usergroups, 5 classifiers — 73 KB, filter it down). ## Documents: protocols, opinions, amendments `/api/documents` searches every registered document; `/api/documents/{uuid}` adds `files[]` with `_links.download.href` and `volume` (the sitting the document belongs to). - **Only the 20 types in `/api/lists/document-types` are actually searchable.** The query accepts 42, but `seletuskiri`, `muudatusettepanek`, `opinionDocument`, `algtekst` and `lopptekst` return `totalElements: 0` with any date window — those are bill attachments and are reachable ONLY through the draft-detail response above. Types that do work: `protokoll` (2 318 since 2024), `letterDocument`, `decisionDocument`, `commissionOpinionDocument`, `aruanne`, `collectiveAddressDocument`, `interpellationsDocument`, `writtenQuestionDocument`, `lisadokumendid`, `yldine`. - `title` is a case-insensitive substring: `?title=põhiseaduskomisjoni istungi protokoll&documentType=protokoll` plus a date window → 7 hits, 2.8 KB. - **`createdStart`/`createdEnd` filter the registration date, not the sitting date** — a protocol of the 4 June sitting was registered on 10 June. Widen the window by a week and read the real date from `volume.title`. - 1000-signature petitions: `/api/documents/collective-addresses` returns all 219 with their full status trail (`statuses[]` — registered → taken into proceedings → committee decision → closed) — but it **ignores `size`** and always sends 1.4 MB, so filter it down rather than reading it whole; a single petition via `/api/documents/collective-addresses/{uuid}` is 7 KB. ## Stenograms (chamber debate) `/api/steno/verbatims?startDate=…&endDate=…` returns `[{date, title, plenarySession, agendaItems[]}]`, and each agenda item has `events[]` of `{type: "SPEECH", date, speaker, text}` — speech-level, with the speaker as a plain string. One sitting day is ~185 KB, so filter it and filter by `speaker` or by agenda-item title. `type` (`IS`/`IT`/`IK`) is accepted but has no effect. ## Request contract - No authentication. Verified 2026-08-02 against OpenAPI version `2.21.8`. - `startDate`/`endDate`/`createdStart`/`createdEnd` use `yyyy-MM-dd`. Both dates are required for `/api/votings` and `/api/steno/verbatims`, optional for `/api/agenda/plenary`. - Draft search accepts `title`, `reference`, `mark`, `membership`, `draftTypeCode`, `proceedingStatus`, `activeDraftStage`, `leadingCommitteeUuid`, `responsibleMemberUuid`, `initiatorUuid`, initiation/amendment date bounds, `page`, `size`, `sort`. - `draftTypeCode` accepts `UA`, `DE`, `PE`, `AE`, `TK`, `SE`, or `OE` (`SE` = seaduseelnõu). Use the classifier codes; do not translate them. - `lang` accepts `ET`, `RU`, or `EN` and defaults to `ET` where supported. - Paged endpoints (`/api/volumes/drafts`, `/api/documents`) answer `{_embedded:{content:[…]}, page:{size, totalElements, totalPages, number}}`. `/api/usergroups`, `/api/votings`, `/api/steno/verbatims` and `/api/lists/*` answer a bare array and do not page. - **HTTP 429 is rate limiting, not an unavailable endpoint. Pace any loop at about one request per second** — measured 2026-08-02: back to back, 1 request in 25 got through and 24 were 429; at 500 ms, 13 of 25; at 800 ms, 40 of 40 clean. ## Downloading files - `_links.download.href` on any file → `https://api.riigikogu.ee/api/files/{uuid}/download`. Check `accessRestrictionType`: anything other than `PUBLIC` will not open, and that is a fact to report, not a failed retrieval. - Prefer the `.pdf` entry. The `.asice` beside it is a ZIP wrapping the very same PDF plus signatures — unzipping it gains nothing. Read PDFs with `pdfText(url)` from `pdf.mjs`. - Some `.html` files (e-mail exports, e.g. committee correspondence) come back as `Content-Type: text/html` with **no charset** while the bytes are **windows-1257**. `res.text()` turns every õ/ä/ö/ü into `�` — 79 of them in one 6 KB letter. Decode explicitly: `new TextDecoder("windows-1257").decode(Buffer.from(await res.arrayBuffer()))`. ## The sitting calendar bounds every date range **RKKTS § 45 lg 1: korralised istungjärgud run from the second Monday of January to the third Thursday of June, and from the second Monday of September to the third Thursday of December.** Outside those windows the chamber does not sit, so an empty result is correct rather than a failed retrieval — which is why a query for "June 2026" legitimately stops at **18 June**, and why a year has no bills initiated in July or August. When a range you were asked about runs past the end of a session, say so and name the reason, or the citizen reads the last date in the data as the last thing that happened. Check the boundary against the year in question rather than assuming a date. ## Reporting - Preserve UUIDs, draft marks, stages, classifier codes, timestamps, sitting metadata, speaker names and vote values as returned, and cite the exact URL you fetched. - **Give the citizen a page they can open.** `api.riigikogu.ee` is a machine endpoint; a bare API URL is not a usable citation for a person. Every module result carries `page` for this; the human pages take the SAME uuid the API returns — bill `https://www.riigikogu.ee/tegevus/eelnoud/eelnou/{draftUuid}/`, member `https://www.riigikogu.ee/riigikogu/koosseis/riigikogu-liikmed/saadik/{memberUuid}/`. Cite the human page for the reader, keep the API URL as evidence, and never offer `https://api.riigikogu.ee/` on its own. - Do not infer that a scheduled agenda item was completed; join to votes, stenograms or documents when outcomes matter. - An `opinions[]` entry proves someone filed a document, not that they were heard or that anything changed — say which it is. ## Module API memberVotings(name, {from, to}) -> {source, page, member, truncated, presenceChecks, votings, rows, totals} sittingVotings({from, to}) -> {source, totals, sessionNote, sittings, rows} draftTrace("418 SE") -> {source, mark, title, stage, initiated, introduction, initiators, leadingCommittee, counts, opinions, amendments, readings, texts} committeeSittings("põhiseaduskomisjon", {from, to}) -> {source, committee, totals, sittings} draftStats({from, to, byProposer}) -> {source, page, asOf, note, totals, proposers, months, stages, proposerNote, truncated, cohortTotal}