Access model
The API is not open to the public internet. Two things exist, and they are different on purpose:
| What | Who can reach it | What it is for |
|---|---|---|
/api/screen-demo on this site |
anyone, 10 requests/min, limit=3 fixed |
a public demo of sanctions screening — one handle, trimmed output |
the full /v1/* surface |
contracted integrators only | everything on this page |
Integrators reach the API the way our own assistant product does: we provision a
dedicated tunnel onto your host, and your service talks to
http://127.0.0.1:8788 as if the corpus were local. Nothing of yours is exposed to the
internet, and nothing of ours is either — the database never leaves our box.
# what we ship you (systemd, one command to install)
lexatlas-tunnel@yourservice.service # the tunnel itself, auto-restart
lexatlas-tunnel-check@yourservice.timer # liveness check: is the socket answering, not just running
# after install, from your host:
curl -s -H "X-API-Key: $LEXATLAS_KEY" http://127.0.0.1:8788/v1/health
Ask for access through Contact. You receive: an API key, the tunnel kit,
your jurisdiction scope, and the OpenAPI document at /openapi.json (/docs
serves it as a page).
Base URL in every example below: http://127.0.0.1:8788 — your end of
the tunnel. Legacy short paths (/get, /search, …) still answer, but write new
integrations against /v1/*: that is the versioned surface we hold stable.
Authentication
One header. No OAuth dance, no session.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/norm/ru:gk_rf_1:395"
Two endpoints answer without a key, so a broken key is diagnosable:
/v1/health and /v1/errors.
Rate limits are a bucket, not a calendar minute
curl -s -H "X-API-Key: $LEXATLAS_KEY" http://127.0.0.1:8788/v1/limits
{
"key_name": "your-service",
"rate_per_min": 300,
"active": true,
"org": { "slug": "your-org", "name": "Your Company" },
"remaining_this_minute": 296,
"batch": {
"norms_max_cites": 50,
"citecheck_batch_max_documents": 20,
"answer_pack_max_fragments": 20,
"changes_max_limit": 1000,
"max_body_bytes": 1048576
},
"rate_model": {
"kind": "token_bucket",
"refill_per_min": 300,
"burst": 300
},
"on_limit": "HTTP 429 + Retry-After (seconds until the next token, not until the minute ends)"
}
The window is counted per key, not per host. If you run several workers, they
share the bucket. On 429, honour Retry-After — it is the wait for the next
token, so sleeping to the top of the minute is both wrong and slower.
The trust envelope
Every answer that carries legal text carries, in the same response, where it came from, whether it is in force, whether you may quote it verbatim, and how confident we are. This is not decoration: a legal answer without provenance cannot be defended in a dispute, and vendor due-diligence will ask you for exactly these fields.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/norm/ru:gk_rf_1:395"
{
"cite": "ru:gk_rf_1:395",
"title": "Ответственность за неисполнение денежного обязательства",
"act_title": "Кодекс Российской Федерации от 30.11.1994 № 51-ФЗ «Гражданский кодекс …»",
"status": {
"value": "in_force",
"operative": true,
"confirmed": true,
"basis": "source_status",
"evidence": "статус акта у источника: «Действует c изменениями»"
},
"language": {
"lang": "rus",
"status": "authentic",
"authentic": true,
"note": "язык принятия — текст имеет юридическую силу"
},
"confidence": "official_consolidated",
"provenance": {
"source": "pravo.gov.ru/ips",
"official_url": "http://pravo.gov.ru/proxy/ips/?docbody=&nd=102033239",
"license": { "reuse": "reconstruct", "regime": "public_domain", "quote_verbatim_allowed": true }
},
"knowledge_cursor": 1691710
}
| Field | Read it as |
|---|---|
status.value | in_force · repealed · suspended · not_yet_in_force. basis tells you how we know; confirmed:false means the state is inferred, not stated by the source. |
language.authentic | whether this text is the language of adoption. Verbatim quoting, point-in-time and sanctions work only on authentic text; translations are a marked layer, never a substitute. |
confidence | how the text was obtained: official_consolidated is the strongest, reconstructions are labelled as such. |
provenance.license.reuse | redistribute · reconstruct · cite_only · hold. Your redistribution rights follow this field, per act. |
knowledge_cursor | the corpus clock this answer was computed against — see below. |
Time, cursors and point-in-time
Two different clocks, and confusing them is the classic integration bug:
- Legal time —
as_of=YYYY-MM-DD: the law as it stood on that date. - Knowledge time —
knowledge_cursor: how much we already knew when the answer was produced. It only ever grows.
# the corpus clock right now
curl -s -H "X-API-Key: $LEXATLAS_KEY" http://127.0.0.1:8788/v1/knowledge
{ "at": "2026-08-20T20:38:50Z", "changes_cursor": 1691710 }
# the rule as it stood on a past date
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/norm/ru:gk_rf_1:395?as_of=2019-01-01"
Store the cursor with anything you cache. When you re-check, ask
/v1/changes?cursor=<stored> and you get exactly what happened since — no polling of
whole documents, no diffing on your side.
Retrieval
GET/v1/norm/{cite}
One article: text, act, status, freshness, provenance. Parameters: lang, as_of.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/norm/ru:gk_rf_1:395?lang=rus"
The cite format is jurisdiction:act_code:article. Act codes are ours and
have changed before — if you store anything long-term, store the requisites and resolve
through /v1/act instead.
POST/v1/norms
Up to 50 cites in one call — use it instead of a loop; it is one rate-limit token, not fifty.
curl -s -X POST -H "X-API-Key: $LEXATLAS_KEY" -H "Content-Type: application/json" \
-d '{"cites":["ru:gk_rf_1:395","ru:gk_rf_1:196"],"lang":"rus"}' \
http://127.0.0.1:8788/v1/norms
GET/v1/verify/{cite}
The cheap question: does this citation exist, is it in force, may it be quoted verbatim?
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/verify/ru:gk_rf_1:196"
{ "cite": "ru:gk_rf_1:196", "exists": true, "in_force": true, "status": "in_force" }
GET/v1/history/{cite}
Amendment history of an article: when it changed and by which law. This is the layer that cannot be reconstructed after the fact — it exists because we recorded it as it happened.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/history/ru:gk_rf_1:395"
GET/v1/act · GET/v1/act/toc
Resolve an act by its requisites (date + number), then list its structure. Requisites do not age; codes do.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/act?jur=ru&date=1994-11-30&no=51-%D0%A4%D0%97"
{
"jurisdiction": "ru",
"query": { "date": "1994-11-30", "number": "51-ФЗ", "number_matched_as": "51" },
"found": true,
"count": 3,
"acts": [
{ "cite_prefix": "ru:gk_rf_1", "is_canonical": true, "in_force": true,
"kind": "codex", "text_available": true, "articles": 594, "articles_indexed": 594 }
]
}
Search & answers
GET/v1/search
Hybrid retrieval — full-text and vector channels fused by RRF, across jurisdictions.
Parameters: q, jur, k, plus act to restrict to a single act.
curl -s -H "X-API-Key: $LEXATLAS_KEY" -G \
--data-urlencode "q=ответственность за неисполнение денежного обязательства" \
--data "jur=ru&k=5" \
http://127.0.0.1:8788/v1/search
Each row carries cite, act_code, act_title, act_kind,
status, language, signal (which channels matched) and rrf.
Two response fields are worth wiring into your code from day one:
act_filter.known— present always when you filter by act. An unknown act code returns an empty list withknown:false: that is a fact about our corpus, not about the law.situation_used— present when the question matched a verified institution mapping and we therefore asked our index a different, shorter question. Your text is never rewritten;matchtells you whether it was recognised exactly or generalised.
POST/v1/answer_pack
Everything an answer needs in one call: ranked fragments, mandatory citations, the outcome verdict and the envelope. This is what a legal assistant should call instead of assembling five requests.
curl -s -X POST -H "X-API-Key: $LEXATLAS_KEY" -H "Content-Type: application/json" \
-d '{"q":"оплата товара по договору поставки просрочка оплаты покупателем","jur":"ru","k":8}' \
http://127.0.0.1:8788/v1/answer_pack
{
"outcome": "supported",
"situation_used": { "primary": "оплата товара покупателем", "match": "exact" },
"must_cite": [ { "cite": "ru:gk_rf_2:486" }, { "cite": "ru:gk_rf_2:516" }, { "cite": "ru:gk_rf_1:395" } ],
"fragments": [
{ "cite_base": "ru:gk_rf_2:486", "relevance": { "grade": "strong" } },
{ "cite_base": "ru:gk_rf_2:516", "relevance": { "grade": "companion" }, "companion_of": "расчёты за поставляемые товары" }
]
}
Read relevance.grade as a closed enumeration and treat an unknown value as a
refusal. strong means the text lexically confirms the question;
companion means the row was pulled in as the neighbouring institution and is
not lexical confirmation. Products that read only "is it strong?" have mis-reported
companions as confirmed — that exact defect was found and fixed on 19–20 August 2026.
Change & calendar
GET/v1/changes
The changefeed. Ask by cursor (recommended) or by since date;
jur and limit narrow it.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/changes?cursor=1691000&jur=ru&limit=2"
{
"since_cursor": 1691000,
"summary": { "article_added": 1269587, "article_modified": 300006, "article_removed": 17798 },
"cause_summary": { "source_change": 1384240, "artifact_fix": 136758, "reparse": 66845 },
"cause_note": "source_change — the law itself changed; any other cause is OUR correction of the
same original text. If you only want legal changes, filter on `cause`.",
"items": [ { "cite": "ru:…", "change": "article_modified", "at": "…", "cursor": 33910 } ],
"order": "cursor_asc",
"page": { "limit": 2, "returned": 2, "truncated": true }
}
Filter on cause. Most integrations want source_change only —
otherwise our own re-parsing shows up in your product as "the law changed", which it did not.
GET/v1/calendar/upcoming · GET/v1/calendar/stats
What comes into force next, with the sentence that says so. A predictive fact, quoted, not inferred.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/calendar/upcoming?jur=ru&months=3"
{
"from": "2026-08-20", "to": "2026-11-19", "jur": "ru", "count": 544,
"events": [
{ "date": "2026-09-01", "cite": "ru:ru_fz105_2026:3",
"snippet": "Настоящий Федеральный закон вступает в силу с 1 сентября 2026 года." }
]
}
Coverage & trust
GET/v1/coverage/{jur}
Provable completeness, not a marketing number: the source's own count, our count inside the same slice, and the evidence URL the count was read from.
curl -s -H "X-API-Key: $LEXATLAS_KEY" http://127.0.0.1:8788/v1/coverage/kz
{
"jurisdiction": "kz",
"known": true,
"universe": [ { "doc_type": "документы", "source_total": 229237, "our_total": 228271,
"measured_at": "2026-08-20", "evidence": "https://adilet.zan.kz/… → regex" } ],
"catalog": { "rows": 228271, "with_source_status": 228244, "pct_of_universe": 99.6 },
"corpus": { "acts": 74, "articles": 19821, "articles_indexed": 19821 }
}
catalog is what we have catalogued; corpus is what we have with
full text and index. They are different numbers and we never merge them.
GET/v1/transparency/{jur} · GET/v1/authority/{jur} · GET/v1/observed/{jur} · GET/v1/brief/{jur}
The observation layer around the corpus:
- transparency — publication lag and how much of a jurisdiction is machine-observable;
- authority — the state body map and who is the official publisher for each act type;
- observed — what our observation points actually saw (issues, acts, bills), before parsing;
- brief — the daily country brief in three planes: legislation, media, pipeline.
curl -s -H "X-API-Key: $LEXATLAS_KEY" "http://127.0.0.1:8788/v1/brief/kz?date=2026-08-20"
null in these responses means "not measured", never zero. We keep the two
apart deliberately: a threshold that treats "unknown" as "none" turns silence into a false claim.
Compliance
GET/v1/screen
Multipolar sanctions screening — US OFAC, UK OFSI and EAEU lists, matched by name, alias or registration number, quoted verbatim from the list.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/screen?q=Rosneft&limit=1"
{
"query": "Rosneft", "hits": 1, "total_matches": 6, "truncated": true,
"by_jurisdiction": { "us": 1 },
"matches": [
{ "jurisdiction": "us", "authority": "us_ofac",
"list_entry": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY",
"subject_type": "entity", "status": "in_force",
"restriction_kind": "financial_designation",
"match_via": "alias_exact", "match_score": 1.0,
"legal_basis": "UKRAINE-EO13662, RUSSIA-E…" }
]
}
No hit is not a clean bill. The response says which lists were screened
(screened_lists) and states this explicitly. Screening a name we do not carry
produces an honest empty answer, not a negative finding.
POST/v1/citecheck · POST/v1/citecheck/batch
Give it a document; it finds every legal reference in the text and checks each one against the corpus: does it exist, is it still in force, was it renumbered. Batch takes up to 20 documents.
curl -s -X POST -H "X-API-Key: $LEXATLAS_KEY" -H "Content-Type: application/json" \
-d '{"text":"Согласно ст. 395 ГК РФ проценты начисляются на сумму долга.","default_jur":"ru"}' \
http://127.0.0.1:8788/v1/citecheck
{
"total": 1,
"tally": { "ok": 1, "repealed": 0, "not_found": 0, "unresolved": 0 },
"flagged": 0,
"findings": [
{ "raw": "ст.395 ГК РФ", "article_no": "395", "jurisdiction": "ru",
"code_abbrev": "ГК", "cite": "ru:gk_rf_1:395", "verdict": "ok" }
]
}
This is the cheapest integration with the highest visible value: run it over drafts your product already produces and you catch dead references before your user signs them.
Domain feeds
GET/v1/pipeline
The legislative pipeline across the CIS: bills and laws by stage, with links to the resulting act
when it appears. Parameters: jur, stage, since,
linked_only, limit.
curl -s -H "X-API-Key: $LEXATLAS_KEY" "http://127.0.0.1:8788/v1/pipeline?jur=ru&limit=2"
GET/v1/notary/tariffs
Regional notary tariffs of the Russian Federation, by subject and scope, with the approving
document behind each figure. Parameters: subject, scope,
changed_since, limit.
curl -s -H "X-API-Key: $LEXATLAS_KEY" \
"http://127.0.0.1:8788/v1/notary/tariffs?subject=77&scope=all"
GET/v1/rates
Central bank policy rates — the value that was in force on a given date, plus the full history of change points. A policy rate is an observation with a date, not a norm: it is set by a board decision, so it has no articles and no editions. What we add is not the number — anyone can read that off the central bank site — but the basis on which it applies, returned with a verbatim quote from the law.
as_of is the point of this endpoint: an interest claim under art. 395 of the Russian
Civil Code needs the rate of the period of default, not today's. in_force_to is
derived from the next change point; null means "no end yet", not "forever". An
as_of before the first known point returns found: false and says where the
series starts — returning the earliest value instead would pass "we do not know" off as "that is what
it was". Parameters: jur, series, as_of, history.
curl -s -H "X-API-Key: $LEXATLAS_KEY" "http://127.0.0.1:8788/v1/rates?jur=ru&as_of=2022-03-15"
{
"jurisdiction": "ru",
"found": true,
"rates": [
{
"id": "ru_key_rate",
"title": "Ключевая ставка Банка России",
"unit": "% годовых",
"as_of": "2022-03-15",
"value": "20.00",
"in_force_from": "2022-02-28",
"in_force_to": "2022-04-10",
"is_current": false,
"agency": { "short_name": "ЦБ РФ", "url": "https://www.cbr.ru/" },
"basis": {
"act": "ru:fz86_rf",
"article": "37",
"verbatim": "Банк России может устанавливать одну или несколько процентных ставок…"
},
"coverage": {
"first_change_point": "2013-09-17",
"last_change_point": "2026-07-27",
"change_points": 66
},
"license": "unknown"
}
],
"knowledge": { "at": "2026-08-30T01:52:10Z", "changes_cursor": 1812164 }
}
Source licence is unknown until we review the terms of use of the source —
the field ships with every row so that it does not surface at procurement time.
Account & ops
| Endpoint | What it answers |
|---|---|
GET/v1/health | service state and the full endpoint list — no key required, so you can tell "key is wrong" from "service is down" |
GET/v1/errors | the error contract itself, served by the API — no key required |
GET/v1/limits | your rate, your batch ceilings, your jurisdiction scope |
GET/v1/usage | your own consumption — the same numbers we bill by. Optional client_tag lets you split usage per internal tool; scope is your organisation's keys only |
GET/v1/account | keys of your organisation, their scopes and state |
POST/v1/account/keys | mint a key (shown once) |
POST/v1/account/keys/{id}/rotate | rotate — old key stays valid for the grace window |
POST/v1/account/keys/{id}/revoke | revoke immediately |
by_client_tag names the caller, not the tenant it served.
We store the X-Client-Tag header exactly as you send it, once per request, and we
do not infer anything from it. That makes the breakdown exact for request-driven endpoints —
/v1/search, /v1/answer_pack, /v1/norms, /v1/act —
where one call serves one tenant.
It does not hold wherever your side collapses many tenants into one call — a change-feed sync or cache refresh that runs once per process, a nightly batch, a warmed index. There the request carries the tag of whichever tenant happened to trigger it, so the row reads as if that tenant made every call, and a per-department bill built on it will be plausible and wrong. Whether that applies to you is a fact about your client, not about this API: for each endpoint you bill on, check whether the call that reaches us is made per tenant or once for many. Where it is shared, send a tag that names the job rather than the tenant that woke it, and read that row as a per-process cost.
Two-way exchange — how our own consumer is wired
An integration is not finished when data flows one way. Our assistant product runs against these two feeds, and we recommend the same shape to every integrator: it turns "we think the corpus is wrong" into a reproducible, machine-tracked conversation.
GET/v1/deploys?since=<cursor>
Our deployment journal. Every change we ship that can affect your answers appears here with a monotonic cursor, so your regression suite can start itself when — and only when — something moved.
curl -s -H "X-API-Key: $LEXATLAS_KEY" "http://127.0.0.1:8788/v1/deploys?since=108"
{
"deploys": [
{ "cursor": 110,
"deployed_at": "2026-08-20T19:19:02Z",
"closes": [ { "id": "P-2026-08-19-038",
"check": "ops/test_situation_layer.py + ops/check_enumerations.py",
"available_via_api": true } ],
"retracts": [],
"note": "…what changed, in plain words…" }
]
}
closesis always present, empty list included — "nothing closed" and "field missing" are different facts;available_via_apisits on each identifier: "fixed" and "reachable by you" are separate events, and the difference once cost a consumer a day of downtime;retractscarries statements we withdraw — knowledge that travelled by machine has its cancellation travel the same way.
POST/v1/findings · GET/v1/findings?since=<cursor>
Send us a defect with the same key you read with. We triage it hourly, automatically.
curl -s -X POST -H "X-API-Key: $LEXATLAS_KEY" -H "Content-Type: application/json" \
-d '{
"finding": {
"id": "YOURPREFIX-2026-08-20-001",
"blocking": false,
"title": "expected norm does not reach top-5 for a supply-contract question",
"class": "corpus.rank.norm_not_reached"
},
"cases": [
{ "task": "T001",
"corpus_query": "оплата товара по договору поставки просрочка оплаты покупателем",
"jur": "ru",
"expected": ["ru:gk_rf_2:486", "ru:gk_rf_2:516"],
"observed": ["ru:gk_rf_2:489"] }
]
}' \
http://127.0.0.1:8788/v1/findings
Three fields are mandatory, and the reason is the same for all three — a finding we cannot reproduce by machine is a finding we cannot close:
finding.id— your identifier. We close it under your number, not ours;finding.blocking— boolean, explicitly;cases[].corpus_query— verbatim, exactly the string you sent us.
Then read the state back with GET /v1/findings?since=<cursor>: statuses move
through получено → воспроизведено → закрыто, and the closing
deploy appears in /v1/deploys with your id in closes.
Errors & enumerations
curl -s http://127.0.0.1:8788/v1/errors # no key needed: the contract describes itself
| Code | Meaning | What to do |
|---|---|---|
400 | malformed request — the body says which field and why | fix the request; the message is machine-readable (error_code) |
401 | key missing or invalid | check the header; /v1/health answers without a key |
404 | route does not exist | note: an unknown act code is not a 404 — it is a 200 with act_filter.known:false |
429 | rate limit | sleep Retry-After seconds |
5xx | our fault | retry with backoff; if it persists, send a finding |
Treat every enumeration as closed, and an unknown value as a refusal — never as success. We extend enumerations only with an announcement in the deploy journal first, and we guard that with a static check on our side. Your side should fail loudly rather than guess what a new value means.
Integration checklist
What we would verify before calling an integration finished — this is the list our own consumer went through:
- Tunnel liveness is checked, not assumed. A running unit is not an answering socket. Ship the timer, not just the service.
- Every stored answer keeps its
knowledge_cursor. Without it you cannot tell a stale cache from a changed law. - Your refresh reads
/v1/changes?cursor=and filters oncause=source_change. - Your regression suite is triggered by
/v1/deploys, and the cursor you last processed is persisted. A deploy without a journal line does not exist for you — and we consider such a deploy unshipped. - Enumerations are closed on your side (
status,relevance.grade,cause,outcome), unknown value ⇒ refusal. - Verbatim gate. Quote only authentic-language text, and only where
license.quote_verbatim_allowedis true. - Defects go through
POST /v1/findingswith a verbatimcorpus_query, not through prose. Prose is welcome too — it just cannot be replayed. - Rate handling honours
Retry-Afterand shares the bucket across workers.
Limits & licensing
- Jurisdiction scope is part of your key.
/v1/limitsshows what your key may reach; widening it is a tariff question, not a technical one. - Redistribution follows
provenance.license.reuse, per act — not per jurisdiction and not per contract.cite_onlyacts may be cited, never republished, and never translated by you. - Translations are a separate, marked layer, always served next to the original and never instead of it.
- Personal data. Content classified as
personal_datais gated by the entitlements on your key. - Fair use of the shared window. The rate window is per key and shared by your workers; load tests are agreed in advance so they do not land on other integrators.
Need access, a wider scope, or a review of your integration plan? Talk to us — we will send the tunnel kit, a key and the OpenAPI document.