--- name: open-data description: Discover Estonian public datasets through the national data portal when no source-specific recipe is known. module: avaandmed.mjs execution: get --- # National Open Data Discovery The national catalogue at `andmed.eesti.ee` — 5,718 datasets on 2026-08-02, from every ministry, agency and municipality that publishes any. **This is a catalogue, not the data.** A row says who publishes something, when they last touched it, under what licence, and where the file is. Reaching the data means opening the dataset and following its distribution URL. Use this when no source-specific recipe covers the question; when one does, it will answer better. ## Access - Search: `GET https://avaandmed.eesti.ee/api/datasets?search=&limit=&page=` — JSON, no authentication, `{data: [...], metadata: {page, limit, total}}`. - One dataset: `GET https://avaandmed.eesti.ee/api/datasets/slug/` — the full record, including `distributions`. - Human pages: `https://andmed.eesti.ee/datasets/` (the API host 301s here). Cite these, not the API. The portal **validates its query**: a field it does not know is an HTTP 400 naming it (`property title should not exist` — verified 2026-08-02 for `title`, `q`, `keyword`, `organization`, `categories`, `format`, `status` and six more), and `sortOrder` outside ASC/DESC comes back listing what it accepts. So a filter here cannot silently do nothing, which is not true of most registers. The accepted set is small: `search`, `limit` (max 75), `page`, `sortBy` (`title`, `updatedAt`, `createdAt`), `sortOrder`. A `sortBy` naming a field that does not exist is an HTTP 500, so do not experiment with it. ```probe-limit claim a field it does not know is an HTTP 400 naming it # `title` is the field an agent reaches for first and the portal does not have. # The 400 names it back, which is the property this bullet is really about: a # filter here cannot silently do nothing, so a result from this API is a result # from the query that was actually sent. GET https://avaandmed.eesti.ee/api/datasets?limit=3&title=rahvastik expect-status 400 expect /property title should not exist/ # The control is the accepted spelling of the same intent. It must answer 200: # an API answering 400 to everything would confirm this claim while the portal # was simply down. control GET https://avaandmed.eesti.ee/api/datasets?limit=3&search=rahvastik ``` ## Retrieve 1. `openDatasets(term)` — search. `totals.datasets` is the portal's own count over the whole match; the rows are one page of it. 2. `openDataset(slug)` — the dataset itself: publisher (with registry code), last update, categories, and every distribution with its format, licence, byte size and **complete download URL**. 3. Hand a distribution's `url` to `fileText()`. Never compose a download URL: a portal-hosted file is addressed by a pair of uuids that cannot be derived from the slug. Search the words the dataset itself would use, in Estonian. The listing leaves `organization` empty on most rows — the publisher is on the dataset record, so open it before saying who publishes anything. ## Return Dataset title, publisher, the dataset-page URL, update date, licence, format and the direct data URL — plus which recipe to use next when the dataset points at a source that has one. Do not return the whole catalogue. ## Limits - A catalogued dataset can have no working distribution at all. `accessUrlBroken` is the portal's own flag for a link it could not reach, and `totals.working` counts the distributions that carry a URL — a dataset can be published and unavailable at once. - A `format: "OTHER"` distribution usually points at another service's page (Statistikaamet's tables, for instance), not at a file. - Descriptions are written by the publisher and vary. Verify the final API or file before claiming data access. - Fallback discovery only. When a source-specific recipe exists it knows the register's quirks, and this does not. - There is also an RSS feed at `https://andmed.eesti.ee/api/rss/feed` (3.1 MB, `application/rss+xml`, still live 2026-08-02). It carries update notices rather than the catalogue's own fields, so the JSON API above is both smaller per question and richer. ## Verify - A real search answers HTTP 200 with `metadata.total` and a `data` array. `total: 0` is a genuine no-match; there is no shape here where rows are missing but a count is not. - A dataset slug that does not exist answers **HTTP 404** `Dataset not found or doesn't exist` — an empty record is not how this portal says no. ```probe-limit claim A dataset slug that does not exist answers **HTTP 404** GET https://avaandmed.eesti.ee/api/datasets/slug/sellist-andmestikku-ei-ole-olemas expect-status 404 expect /Dataset not found or doesn't exist/ # The control is a search that must come back 200. The 404 on its own would go # on passing if the whole API started answering 404, and then this guide would # be telling the agent a healthy-sounding story about a portal that is down. control GET https://avaandmed.eesti.ee/api/datasets?limit=1&search=rahvastik ``` - Before calling a file available, fetch it: the distribution's `byteSize` should match what arrives (verified 2026-08-02 — `koondamised`'s XLSX distribution reports 73,946 bytes and downloads 73,946). ```probe GET https://avaandmed.eesti.ee/api/datasets?limit=3&search=rahvastik expect /"total":\s*\d+/ expect /"slug":/ # The counter-example is a field this API does not accept. It must FAIL the # assertions above by being an HTTP 400 that names the field — a register # which instead answered 200 with its whole catalogue is how a run reports # 5,718 datasets as the answer to one question. counter GET https://avaandmed.eesti.ee/api/datasets?limit=3&title=rahvastik ``` ## Module API openDatasets(term, {limit, page, sortBy, sortOrder}) -> {source, page, term, totals, datasets, note} openDataset(slug) -> {source, page, title, description, organization, regCode, updated, published, periodicity, categories, keywords, totals, distributions, note}