--- name: court-proceedings-data description: Query Riigi Teataja's public JSON endpoints for Estonian court decisions and court hearings. module: kohtud.mjs execution: post --- # Court Decisions and Hearings ## Access - Search UI: `https://www.riigiteataja.ee/et/otsing/kohtulahendid` - API base: `https://www.riigiteataja.ee/public-api/api/v1` - Public JSON POST endpoints; no login is required. ## Retrieve Post JSON to `/kohtuteave/otsing/kohtulahendid` for decisions. A minimal request is: ```json { "general": { "searchInText": false, "searchInTitle": false, "searchText": "", "searchText2": "", "logicalOperator": "AND", "morphSearch": false, "sort": "toiminguNr", "sortAscending": false }, "precise": {"kohus": [], "seaduseSatted": {}} } ``` Post the same `general` object to `/kohtuteave/otsing/kohtuistungid` with `precise: {"kohus": []}` for hearings. Useful precise decision fields include `lahendiKpAlgus`, `lahendiKpLopp`, `kohtunik`, `ecliNr`, `menetluseTyyp`, `menetlusliigid`, `kohtumenetluseLiik`, and `kohus`. Hearing fields include `istungiAegAlgus`, `istungiAegLopp`, `istungiLiik`, `istungiSaal`, `kohtunik`, `menetluseLiik`, and `kohus`. Both responses contain `kokku` and up to 30 `tulemused`. Narrow the query with dates or court filters; do not treat the first response as a bulk export. A decision's public file is `GET /kohtuteave/kohtulahendid/{avalikustatudFailiId}/file`. ## Return - `courtDecisions()` rows are `{case, date, court, proceeding, status, statusDate, fileName, fileUrl, annotations, objektId}`; `courtHearings()` rows are `{case, time, court, judge, kind, room, status, title}`. - Read a decision's own text with `fileText(row.fileUrl)`. Riigikohus publishes HTML there, the lower courts PDF; `fileText()` sniffs either. - Do not infer an outcome from a hearing listing or an annotation. ## Limits - Publicity restrictions and redactions apply; this is not access to complete case files. - The result API returns 30 rows at a time and exposes no documented bulk pagination contract. Use focused searches. `leht`, `page`, `offset`, `start`, `from` and `algus` were each tried on 2026-08-01 and every one returned the same first page, so `kokku` (`totals.total`) is the ONLY count. - Dates: the decision endpoint takes plain `YYYY-MM-DD`, the hearing endpoint requires a zoned timestamp (`2026-08-03T00:00:00+03:00`) and answers HTTP 400 to a bare date (re-checked 2026-08-02). The module handles both. ```probe-limit claim the hearing endpoint requires a zoned timestamp (`2026-08-03T00:00:00+03:00`) and answers HTTP 400 to a bare date # Both halves of the sentence, in order: the decision endpoint takes the bare # date, the hearing endpoint refuses it, and the control is that SAME hearing # request with the SAME date in the zoned form — so a day when the hearings # endpoint answered 400 to everything cannot read as this claim holding. POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtulahendid body-json {"general":{},"precise":{"lahendiKpAlgus":"2026-01-01"}} expect-status 200 POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtuistungid body-json {"general":{},"precise":{"istungiAegAlgus":"2026-08-03"}} expect-status 400 # `general` is load-bearing even when empty: a body without that key answers # HTTP 500 on both endpoints, and 500 is a status the kohus block below # asserts — so a trimmed body would confirm a claim for the wrong reason # (measured 2026-08-02). control POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtuistungid body-json {"general":{},"precise":{"istungiAegAlgus":"2026-08-03T00:00:00+03:00"}} ``` - The `kohus` filter takes unpublished classifier ids: passing a court's name answers 400 (decisions) or 500 (hearings) (re-checked 2026-08-02). Narrow by date or text and read each row's `court`. ```probe-limit claim passing a court's name answers 400 (decisions) or 500 (hearings) POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtuistungid body-json {"general":{},"precise":{"kohus":["Harju Maakohus"]}} expect-status 500 POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtulahendid body-json {"general":{},"precise":{"kohus":["Harju Maakohus"]}} expect-status 400 # The control is that same decisions request with the court name REMOVED, which # is what separates "the name is not a classifier id" from "the body is # malformed": a malformed body answers 400 here too and would otherwise confirm # the claim on its own. control POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtulahendid body-json {"general":{},"precise":{"kohus":[]}} ``` - Both endpoints are **JSON-only POST**: the same body sent as `application/x-www-form-urlencoded` answers HTTP 415, and a GET answers HTTP 405 (checked 2026-08-02). ```probe-limit claim the same body sent as `application/x-www-form-urlencoded` answers HTTP 415 # `general=x` is a stand-in body on purpose, and swapping in the real JSON one # breaks this: the API replays a 200 it has already answered for that exact # body, media type and all. A body it had just accepted as JSON came back 200 # under the form Content-Type; a body it had never seen came back 415 in the # same second (measured 2026-08-02). Only a body no probe ever sends as JSON # tests the media type rather than the cache. POST https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtuistungid body general=x expect-status 415 expect /Unsupported Media Type/ GET https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/otsing/kohtuistungid expect-status 405 # The control is the documented GET on the same API: one published decision # file. Without it, a day when the whole public-api refused everything would # confirm both refusals above while the source was simply down. control GET https://www.riigiteataja.ee/public-api/api/v1/kohtuteave/kohtulahendid/451798218/file ``` - Historical URLs ending in `koik_menetlused.html` and `kohtuistungid_otsing.html` now load the same JavaScript application; they are not data endpoints. ## Verify - Require HTTP 200 JSON with integer `kokku` and nonempty `tulemused` from both POST endpoints. - Require decision rows to contain `kohtuasjaNumber`, `objektId`, and `lahendiKuulutamiseAeg`; require hearing rows to contain `kohtuasjaNr`, `kohus`, and `istungiAeg`. ## Module API courtDecisions({text, from, to, judge, ecli, inTitle}) -> {source, page, query, totals, rows, note} courtHearings({from, to, judge}) -> {source, page, query, totals, rows, note}