Kodu, maa ja keskkond
Planeeringute register
Millised detail- ja üldplaneeringud on sinu kodu lähedal menetluses, mis etapis ja kelle käes.
Planeeringute register (planeeringud.ee) koondab kehtestatud üld- ja detailplaneeringud üle Eesti, omavalitsuse kaupa, koos plaani liigi, seisundi, korraldaja ja avalike dokumentidega. Detailplaneeringu avaliku väljapaneku ajal on igal inimesel õigus selle kohta arvamust avaldada, ja just see aken on ajaliselt piiratud. Üleriigiline register käivat väljapanekut ei näita, tema seisundid algavad kehtestamisest: pooleliolevad menetlused ja väljapanekuaknad on Tallinna enda registris tpr.tallinn.ee.
Mida su agent peab oskama
POSTVajab vormipäringut. Vähemalt üks vajalik päring on vormi saatmine (POST). Küpsiseid ega turvamärgiseid vahele ei jää, aga ainult lehti tõmbav agent seda päringut teha ei saa.
Vastused, mis on siit tulnud
- Mida mu linnas praegu planeeritakse?Millised detailplaneeringud on Tallinnas praegu avalikul väljapanekul või hiljuti algatatud?2026-08-03
Juhend ise
Inglise keeles, sest seda loeb mudel.
Estonia Planning Decisions
Vetted module (use this first)
planeeringud.mjs ships in https://kodanikukratt.ee/kratt-agent.tar.gz (plain ESM, Node 22+) and is re-verified against the live source. It answers "what is on public display in Tallinn right now" in two requests:
import { show } from "./kratt.mjs";
import {
tallinnPublicDisplays, tallinnPlans, searchNationalPlans, nationalPlansByAuthority,
} from "./planeeringud.mjs";
// Display windows running or upcoming NOW — `to` is the citizen's objection deadline.
const { source, displays } = await tallinnPublicDisplays();
show({ source, displays });
// Recently initiated detail plans, newest first.
const initiated = await tallinnPlans({ status: "Algatamisel", size: 25 });
show({ source: initiated.source, total: initiated.total, rows: initiated.rows.slice(0, 15) });
// National register (established plans, whole country).
show(await searchNationalPlans("Vanalinna"));
// Established plans for one municipality, by name — the id is resolved for you.
const rae = await nationalPlansByAuthority("Rae vald", { size: 10 });
show({ source: rae.source, authority: rae.authority, total: rae.total, rows: rae.rows });
- Each call returns
source, the URL actually fetched — cite that. tallinnPublicDisplays()reads the Tallinn register'savalikustamine/avalehtfeed — the only source that exposes in-process public-display windows, which is what a citizen can act on.tois null for "Avalik arutelu" rows (single meetings, not display periods).tallinnPlans({status, searchText, locationText, page, size})takes a status name ("Algatamisel", "Avalikustamisel", …) or its numeric code.searchNationalPlans(text)covers the whole country. It has no public-display window, so it cannot answer "what can I object to right now" for anywhere.nationalPlansByAuthority(name)is the per-municipality search: it resolves a body's name (or EHAK code, or registry code) to the register's own authority id and filters on it.nationalPlansByAuthority("Rae vald")gives that municipality's established plans, all types; pass{planType: "DP"}for detailplaneeringud only. It returnstotal,authority(the name the register itself used, so you can see the filter bound to the body you meant) androws.- Also exported:
tallinnPlanStatuses(),nationalPlanDetails(sysid),nationalAuthorities().
Grounding rule: state plan names, planKoods, addresses and window dates only when they appear in output you actually printed. If a fetch fails, say so and link the register rather than inventing a plausible listing. If the Tallinn endpoints start returning 401, anonymous access has closed — fall back to linking https://tpr.tallinn.ee/.
The endpoints below are the fallback if a module call throws.
Access
- Application:
https://www.planeeringud.ee/plank-web/ - API base:
https://www.planeeringud.ee/plank-web/api/ - Public JSON requests require no authentication.
Search
POST JSON to planeering/otsing; control the result page in the URL:
POST /plank-web/api/planeering/otsing?page=0&size=25&sort=kehtestkp,desc
Content-Type: application/json
{"otsistring":"Vanalinna"}
otsistring is the verified free-text filter. The response is a Spring page with content, totalElements, totalPages, and page metadata. Search records include sysid, planid, plannim, abbreviated plan type, authority, lifecycle status, relevant dates, purpose, and map link.
Narrowing to one authority — the field is orgIdList
{"orgIdList":[653]} restricts the search to one korraldaja, and it composes with otsistring. Verified live 2026-08-04: Rae Vallavalitsus (id 653) returns 650 records, 649 of them "planliikNimi" : "DP" and one a üldplaneering, so this is a real per-municipality detailplaneering search rather than a üldplaneering index. Several ids in one array are unioned.
The singular orgId is silently ignored. {"orgId":653} answers HTTP 200 with the whole corpus — the same totalElements as an empty body, 17,415 on 2026-08-04 — and nothing in the response says a filter was dropped. That is the shape of this trap: not an error, but every plan in Estonia wearing the label of the one authority that was asked about. If an authority filter did not narrow the result, it was not applied; check that the rows you print actually carry the korraldaja you asked for.
The id vocabulary is a keyless GET:
GET /plank-web/api/classifier/validautorg
One row per authority — {id, nimetus, registrikood, tasand, kovKood, kovNimi, kehtetu, epost}, 83 rows on 2026-08-04 (79 tasand: "kov", 4 "riik"). Look an id up here rather than storing a name-to-id pair.
id is not the EHAK code, though it looks like one. It equals kovKood for 77 of the 79 municipal rows and differs for two: Sillamäe Linnavalitsus is id 735 against kovKood 736, and Narva-Jõesuu Linnavalitsus is id 514 against kovKood 515. Join on kovKood or registrikood and read the id off the row you matched. Passing an EHAK code straight into orgIdList gives a real, wrong municipality's plans for those two, and nothing about the answer looks wrong.
planseisList and planliikList are the other two verified filters, both taking numeric codes rather than the display names (planliik 30 is DP; planseis 10 is kehtiv). They are typed fields: a wrong value type is rejected outright, where an unknown field name is ignored the way orgId is.
Detail
Fetch GET planeering/{sysid} using sysid from search results. Do not substitute planid; it is a different identifier and is not accepted by this route.
Return at least sysid, planid, name, type, status, organizing authority, purpose, lifecycle dates, planviide, bbox, related plans, and every public planDokuments item with its type, original filename, size, and filePublicUrl.
Naming the citizen's right — say arvamus, not vastuväide
During a detailplaneering's public display, what a person may do is avaldada
arvamust. Write that. PlanS § 135 (2) gives the right to "avaldada
detailplaneeringu kohta arvamust", and the current Planeerimisseadus uses
arvamus 191 times and vastuväide not once (measured 2026-08-03 against
the redaction in force; in the 2012 redaction it was 25 to 1).
vastuväide is not a dead word and is not wrong everywhere — it is statutory in
HMS § 40 and § 47, so an objection inside an administrative proceeding is
still a vastuväide. It is wrong for this one thing, which is the thing citizens
ask about most.
Expect the reader to say it anyway: the press and ordinary speech kept the old
word because it was the law for two decades. Understand vastuväide on the way
in, write arvamus on the way out, and do not correct the person — telling
someone their word is wrong when the register still answers their question is
not what they came for.
Limits
- Status is lifecycle data. Keep the original
planseisNimiorklPlanseisByPlanseis, and do not collapse draft, established, repealed, or paused plans. - Search/export request bodies support more UI filters than the four verified above (
otsistring,orgIdList,planseisList,planliikList); use only fields checked against the live application. An unknown field name is not refused, it is ignored, and the reply is the whole register at HTTP 200 — so a filter is only doing something if the rows come back narrower. - Document links may be PDF, ASiC-E, or another original file type.
- Searching either register is a JSON POST (
planeering/otsinghere,detailplaneering/otsiin Tallinn below), so an agent that can only issue GET requests cannot run those searches. What stays open to it: the Tallinn public-display feed, the quick search and the detail routes, which are all plain GETs. - The national register still cannot answer Tallinn public-display questions, but the reason is narrower than it looks. It CAN be narrowed to Tallinn:
{"orgIdList":[784]}is Tallinna Linnavalitsus and returned 1,900 records on 2026-08-04. What it does not carry is the process.planseis/planseisNimionly ever takes coarse post-establishment values (broneeritud,kehtiv,osaliselt kehtiv,osaliselt peatunud,kehtetu) — there is no in-process "avalik väljapanek" value and the detail response exposes only milestone dates, no display-window start/end. Underneath those five names sit four codes (planseis5, 10, 15, 20) whose row counts add up to the entire register, so the vocabulary really is closed;osaliselt peatunudisplanseis10 withpeatumineset, a display name rather than a sixth state. So: for live Tallinn public displays and freshly initiated plans use the Tallinn register below, and for what has already been established in Tallinn the national register now answers by authority.
Tallinn: Tallinna planeeringute register (tpr.tallinn.ee)
Tallinn's own register gets new detail plans first, and — unlike the national register — it exposes the in-process public-display window that is the citizen's objection window. The SPA lives at https://tpr.tallinn.ee/; its backend under https://tpr.tallinn.ee/api/* answers anonymously today on the read endpoints below (only the edit/menetlus routes are Bearer-gated). Verified live 2026-07-24.
Caveat: these endpoints are withCredentials-flagged in the SPA but currently respond without any token or cookie. If they start returning HTTP 401 WWW-Authenticate: Bearer, the anonymous access has been closed — re-verify and fall back to citing https://tpr.tallinn.ee/ as a pointer rather than inventing data.
Anonymous read endpoints:
GET https://tpr.tallinn.ee/api/avalikustamine/avaleht— JSON array of current & upcoming public displays / discussions (front-page "avalikustamised"). This is the direct answer to "what is on public display in Tallinn right now." Each row:avLiikNimetus—Avalik väljapanek/Eskiislahenduse avalik väljapanek/Avalik arutelu.staatus—TOIMUB(ongoing) orTULEMAS(upcoming).alustamiseAeg/lopetamiseAeg— display window = the citizen objection window (lopetamiseAegis null forAvalik arutelurows, which are single meetings).toimumiskoht,linnaosa,planLiikNimetus,planKood,planNimetus.
POST https://tpr.tallinn.ee/api/detailplaneering/otsi?page=0&size=25with a JSON body — Spring page of detail plans (content,size,totalElements,totalPages,number). Rows carryplanKood,planNimetus,seisund,seisundNimetus,seisundiKp,linnaosa. Body contract fields:isKiirotsing,searchText,locationText,personText,personId,archiveNumber,statusCode,startDate,endDate,tagAktiivesMenetluses,tagKehtivPlaneering,tagYldMuutev,tagTapsustavPt,tagKhsKoostamine,sortKey,sortDirection. Empty{}returns everything (~4589 rows);statusCodefilters to one process stage;locationTextmatches streets/districts;sortKey: "seisundiKp"+sortDirection: "desc"surfaces the most recent stage changes first.GET https://tpr.tallinn.ee/api/classifiers/menseisund/dp/menetluses— thestatusCodevocabulary (detail plans in process):3203Algatamisel,3208Eskiis avalikustamisel,3211Koostamisel,3221Avalikustamisel,3226Kehtestamisel,3231Kehtetuks tunnistamisel.- Supporting:
GET https://tpr.tallinn.ee/api/planeering/kiirotsing?searchText=X(quick text search),GET https://tpr.tallinn.ee/api/detailplaneering/linnaosad/valikud(district picklist),GET https://tpr.tallinn.ee/api/config.
Grounding rule: state plan names, planKoods, addresses, and display-window dates ONLY when they appear in output you actually printed from these endpoints — never from memory or inference. If a fetch fails, say so and link the register; do not fabricate a plausible-looking listing.
Never write a per-plan link for this register (issue #114). tpr.tallinn.ee is a single-page app and EVERY path answers HTTP 200 with the same 83 330-byte shell — the bare root, a real plan's route and a route that never existed are byte-identical (checked from Node 2026-08-02, no per-plan API record exists either). So a …/detailplaneering/<planKood> address can never be confirmed, and a wrong one is indistinguishable from a right one to you and to the reader. Cite https://tpr.tallinn.ee/ and give the planKood and the plan name — that is what lets a citizen find it, and it is what the register's own staff ask for.
What CAN be confirmed is that a plan code exists, and it is the one positive signal this host has:
GET https://tpr.tallinn.ee/api/planeering/kiirotsing?searchText=DP047420→{"dpItems":[{"id":168687,"planKood":"DP047420","planNimetus":"Veerenni tn 20 …"}],…}- the same call with
DP999999→{"dpItems":[],"ypItems":[],"totalDpCount":0,"totalYpCount":0}
Use it to check a code before stating it. It does not make any URL verifiable.
Verify
Require a paged JSON response, ensure detail sysid matches the selected search record, and accept documents only when filePublicUrl is present.
The authority filter is the one capability this register has that is easy to believe you are using when you are not, so it is asserted from both sides:
POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=1
body-json {"orgIdList":[653]}
expect /"korraldaja" : "Rae Vallavalitsus"/
# 653 is Rae Vallavalitsus in classifier/validautorg. The assertion is on the
# korraldaja NAME rather than on a row count on purpose: Rae had 650 records on
# 2026-08-04 and that number moves with every plan established, while the name
# is what actually says the filter bound to the authority asked for.
#
# The counter asks for an id no authority holds and gets HTTP 200 with
# "totalElements" : 0 and an empty content array. That is the same shape a
# working filter returns for an authority with no plans, which is why the
# positive signal has to be a korraldaja and not merely a 200.
counter POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=1
body-json {"orgIdList":[999999]}
And the singular spelling, which is the trap that had this guide telling the agent no authority filter existed at all (written 2026-07, corrected 2026-08-04):
claim The singular `orgId` is silently ignored
# Asked for one authority and answered with all of them, so a page of 25 rows
# contains a korraldaja that is NOT the one requested. That is the stable way to
# say "ignored": pinning `totalElements` to 17,415 would go stale the next time
# a plan is established, and pinning the first row's authority depends on a
# default sort order nothing documents.
#
# The control sends the SAME request with the working spelling. Every row on
# that page is Rae Vallavalitsus, so the lookahead finds nothing and the control
# fails the assertion — which is what proves the filter works and that this
# block is measuring a difference between two spellings rather than a register
# that has simply gone down.
POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=25
body-json {"orgId":653}
expect-status 200
expect /"korraldaja" : "(?!Rae Vallavalitsus)/
control POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=25
body-json {"orgIdList":[653]}
The register holding no in-process plans is the reason a display question has to go to Tallinn's own register, so it is executable too:
claim there is no in-process "avalik väljapanek" value
# Why a handful of codes settles a claim about a whole vocabulary: measured
# 2026-08-04, `planseis` 5, 10, 15 and 20 hold 104, 14,772, 2,476 and 63 records,
# which is 17,415 — the entire register. Four codes account for every row, so
# any other code holds nothing, and the ones below are simply the next slots in
# the register's own 5-step sequence, where an in-process status would sit if
# one were ever added. If this starts returning rows, a sixth state exists and
# the sentence above needs re-reading rather than patching.
#
# The control asks for `kehtiv` on the same endpoint: it comes back far from
# empty, so a register that had gone down or started refusing POSTs cannot
# confirm this limitation by accident.
POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=1
body-json {"planseisList":[25,30,35,40,45,50]}
expect-status 200
expect /"totalElements" : 0/
control POST https://www.planeeringud.ee/plank-web/api/planeering/otsing?page=0&size=1
body-json {"planseisList":[10]}
The Tallinn quick search answers HTTP 200 with empty lists for a code it does not hold, which is indistinguishable from a broken query unless something asserts the difference (issue #82):
GET https://tpr.tallinn.ee/api/planeering/kiirotsing?searchText=DP047420
expect /"planKood":"DP047420"/
# DP047420 is Veerenni tn 20 kinnistu ja lähiala detailplaneering, id 168687.
# The counter asks for a plan code that does not exist and gets
# {"dpItems":[],"ypItems":[],"totalDpCount":0,"totalYpCount":0} — 61 bytes,
# HTTP 200. The day that stops being distinguishable from a real hit, this
# host has no existence check left at all, and nothing about a plan code
# stated from it can be trusted.
counter GET https://tpr.tallinn.ee/api/planeering/kiirotsing?searchText=DP999999
Module API
tallinnPublicDisplays() -> {source, displays} tallinnPlans({status, searchText, locationText, page, size}) -> {total, rows} searchNationalPlans(text, {page, size}) -> {total, rows} nationalPlansByAuthority(nameOrCode, {planType, page, size}) -> {source, authority, total, rows}