Lectorium API
Current status
Lectorium was built as an API - the web interface is a layer on top of it, not the other way round. The same query interface that powers app.lectorium.ee can in principle be integrated elsewhere: into HR software, a client portal, an internal service.
Where each part stands:
- Query interface (search and answer)
- working
- Stable public address
- working
- HTTPS
- working
- API keys
- working
- Self-service key issuance
- planned
- Per-key quotas
- planned
- Terms of use
- planned
The address is https://api.lectorium.ee and every request
needs an API key. That part is finished and working. What's missing is
self-service: you cannot generate a key yourself, you have to get
in touch. Anything marked planned does not exist yet and may change.
Endpoints working
Three endpoints. All take and return JSON.
- GET/healthindex state and threshold - needs no key
- GET/mecheck whether your key works
- POST/searchretrieved provisions, no answer construction
- POST/answerquoted answer with sources
Authentication
The key goes in a header. There is no other way -
/health is the only endpoint that needs no key.
Authorization: Bearer lec_sk_…
Do not put the key in the URL
(?api_key=…). Such a request is rejected with a 400: URLs end
up in server logs, browser history and Referer headers.
To check a key without spending a real query:
curl https://api.lectorium.ee/me \ -H 'Authorization: Bearer lec_sk_…' { "kind": "key", "id": "my-service", "label": "My system" }
Request
curl -X POST https://api.lectorium.ee/answer \
-H 'Authorization: Bearer lec_sk_…' \
-H 'content-type: application/json' \
-d '{"question": "Kui pikk on põhipuhkus?", "top_k": 2}'
question is required, top_k optional
(defaults to 5). Questions are asked in Estonian - that is the language the
indexed law is written in.
asked is optional too — see
clarification below.
Response
This is a real response, not a mock-up - trimmed to a single source:
{
"question": "Kui pikk on põhipuhkus?",
"answer": "§ 55 (Töölepingu seadus): Eeldatakse, et töötaja
iga-aastane puhkus on 28 kalendripäeva (põhipuhkus), kui
töötaja ja tööandja ei ole leppinud kokku pikemas
põhipuhkuses või kui seadus ei sätesta teisiti.",
"sources": [
{
"act": "Töölepingu seadus",
"act_id": "TLS",
"section": "§ 55",
"effective_date": "2026-07-12",
"url": "https://www.riigiteataja.ee/akt/103072026034?leiaKehtiv#para55",
"excerpt": "§ 55 Põhipuhkus Eeldatakse, et töötaja iga-aastane…",
"score": 0.8855
}
],
"confidence": 0.8855,
"disclaimer": "See vastus on koostatud automaatselt Riigi Teataja
tekstide põhjal ega ole õigusnõustamine. Kontrolli alati
viidatud sätete kehtivat redaktsiooni.",
"answered": true
}
When there is no answer
If nothing clears the relevance threshold you still get HTTP 200 - but
answer is null and answered is
false:
{
"question": "mis kell on?",
"answer": null,
"sources": [],
"confidence": 0.0,
"answered": false,
"note": "asjakohast sätet ei leitud"
}
This is not an error - it is the system behaving as designed. See why the answer is a quote.
A note can accompany a successful answer too
The note field is not limited to refusals. When the quoted
provision comes from an act that does not contain the whole rule on its
own, the note arrives alongside the answer - today that means the
Personal Data Protection Act, which only supplements the EU GDPR. As an
integrator, always render note when it is present, including
when answered is true.
Clarification: when the question sits between two acts
When two acts clear the threshold with almost the same score and an
authored case exists for that pair, the response carries an optional
clarify field. The answer itself is entirely normal — quote,
sources and answered: true are all there. The clarification is
an offer, not a failure: skip it and nothing changes.
{
"answer": "§ 15 (Töölepingu seadus): …",
"answered": true,
"sources": [ … ],
"clarify": {
"case": "too-ohutus",
"question": "Kas küsimus puudutab töösuhet või töökeskkonda?",
"options": [
{ "label": "Töösuhe ja tingimused", "payload": "tööandja kohustused töölepingu täitmisel" },
{ "label": "Töökeskkonna ohutus", "payload": "töökeskkonna riskianalüüs ja tööohutuse nõuded" }
]
}
}
Show the user question and the labels.
payload is not user-facing text — it is
retrieval wording. When the user picks an option, send a new request whose
question is the original question + a space + the
payload, with that case in asked:
-d '{"question": "Millised on tööandja kohustused töötaja ees? töökeskkonna riskianalüüs ja tööohutuse nõuded",
"asked": ["too-ohutus"]}'
The client holds the session, not the server: asked is the
only place that state lives. The same case is never offered twice, and no
more than two clarifications are offered in one conversation. The
clarification texts are authored by hand — no language model generates them
— and more are added over time.
Error codes
Errors come back as {"message": "…"}.
400Key was in the URL- use the header
400questionmissing- the field is required
401Key missing or unknown- +
WWW-Authenticate 403Key revoked- request a new one
429Too many requests- +
Retry-After 500Retrieval failed- server-side fault
A missing or wrong key always returns the same message - it does not reveal whether the key was absent, malformed or simply unknown. The distinction would only help an attacker.
Response shape
-
answerstring | null
The quoted provision, formatted "§ N (Act): …".
nullwhen nothing relevant was found. The text is taken from the law, not phrased by a model. -
answeredbool
Whether an answer was found. Check this before displaying
answer. -
sources[]array
The retrieved provisions. When
answeris non-null, this array is never empty. - sources[].actstring Act name in Estonian, e.g. "Töölepingu seadus" (Employment Contracts Act).
-
sources[].act_idstring
Abbreviation, e.g.
TLS,VÕS,PärS. -
sources[].sectionstring
Section, e.g.
§ 55. - sources[].effective_datestring Entry-into-force date of the cited version. Useful if you want to flag recently amended provisions.
- sources[].urlstring Direct link to the current text in Riigi Teataja. Built at index time, not assembled after the fact.
- sources[].excerptstring The provision text, unmodified.
- sources[].scorefloat Cosine similarity to the question. Higher means a more confident match.
- confidencefloat The top match's score.
- disclaimerstring Mandatory notice text. See below.
-
clarifyobject | absent
A clarifying question with options, present when retrieval was undecided between two acts. It never replaces the answer. Fields:
case,question,options[].label,options[].payload. See clarification. - notestring Warning or explanation. On a refusal it says why no answer was produced; on an answer it warns when the quoted provision is not the whole rule (e.g. the Personal Data Protection Act only supplements the GDPR). Always display it.
Rules for integrators
Lectorium returns legal information. If you build your own interface on top of it, some of the system's guarantees pass into your hands - and if you remove them, they stop holding. Please observe these:
-
Always show the citation. An answer without a source is
not a Lectorium answer. If your interface renders
answerbut hidessources, you have removed the only thing that makes the answer checkable. -
Pass the disclaimer through.
disclaimermust reach the user. Estonia regulates who may give legal counsel - that string is not a formatting detail. -
Handle
answered: falseproperly. When nothing was found, say so. Do not substitute your own system's guess, and do not quietly route the question elsewhere. - Do not paraphrase the answer. The entire point is that the text is taken from the law verbatim. Summarising puts you back where we started.
- Do not present it as legal advice - not even implicitly, in a product name or in interface wording.
These are not legal boilerplate; they are the same three rules enforced in Lectorium's own code. We cannot enforce them inside your interface - we can only ask. An integration that ignores them is no longer the system whose accuracy we measured.
API keys working
Keys have been in force since 30 July 2026. Before that the API had no
authentication at all - anyone who knew the address could query it. Now no
request is served without a key, except /health.
-
Key in a header.
Authorization: Bearer lec_sk_…- a key always starts withlec_sk_, which makes it recognisable in a log or a diff. - One key per organisation, not per user - integration happens between systems, not people.
- Only a hash is stored. We do not keep the key itself, so a lost key cannot be recovered by anyone - us included. We issue a new one and revoke the old.
- Several active keys are allowed, so rotation needs no downtime: take a new one, move the integration over, revoke the old.
- Revocation takes effect immediately - the old key starts returning 403 without restarting the service.
We have no need to retain which question anyone asked, and the plan is not to. If that ever changes, it will be written on this page beforehand, not afterwards.
Pricing and limits
The intention is to keep integration free. Lectorium rests on public data - Riigi Teataja texts are published by the state at no cost - and running the service is cheap: the whole thing is one Go service on one server, with no external model API.
To be precise: "free" is currently an intention, not a promise in perpetuity. Realistic limits that may appear:
- Request volume. If one integration starts sending machine-scale traffic, a limit will follow - otherwise everyone else suffers.
- Availability is not guaranteed. This is one server. There is no SLA and no uptime commitment. Do not build a business-critical process that depends on it.
- The response shape may change. Fields may be added. We will give notice before removing or renaming existing ones.
Getting access
Keys exist, but there is no self-service: no form to fill in and no automatic signup yet. To get a key, get in touch and say what you want to build - that also shapes the order in which we build the rest of the access layer.
Contact goes through Crowned Phoenix.
In the meantime you can evaluate the system without any agreement: try the app and read the technical overview, which sets out the measured accuracy and the known failures. If any of those rule it out for your use case, better to find that out first.