{"openapi":"3.1.0","info":{"title":"nu:legal Deutsches Recht API","termsOfService":"https://recht.nulegal.eu/nutzungsbedingungen#terms-en","contact":{"email":"kontakt@nulegal.de"},"license":{"name":"CC BY 4.0 (curated layer)","url":"https://recht.nulegal.eu/license"},"version":"1.0"},"paths":{"/health":{"get":{"summary":"Healthz","operationId":"healthz_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/healthz":{"get":{"summary":"Healthz","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ready":{"get":{"summary":"Ready","description":"Readiness AND liveness ( §6.1 pinned the probe onto this path).\n\nTwo reasons to say no, and they are different failures:\n\n* `_READY` unset — the instance is still warming.'s original meaning;\n  the STARTUP probe waits on it.\n* a stall the watchdog has been measuring for longer than\n  `LAWDB_STALL_READY_SECONDS` (60 s) — the instance warmed fine and has since\n  stopped answering.; the LIVENESS probe acts on it.\n\nDeliberately still a pure in-memory read: no `select 1`, no pool checkout.\nThat is what makes it safe as a liveness target ( §6.1) — a slow\ndatabase must not turn this probe into a fleet-wide restart loop, which is\nexactly what `/health` would do.","operationId":"ready_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1":{"get":{"summary":"V1 Welcome","description":"The front door of the API namespace. Answers 200 with a JSON index of the\nservice — endpoints, licence, contact — unless the client asks for HTML by\nname, in which case it renders the same facts as a minimal English page.\n\nIn the published schema on purpose: an agent that reads /openapi.json to find\nout what this service offers should find the index that tells it.","operationId":"v1_welcome_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/law/{abbrev}":{"get":{"summary":"Get Law","operationId":"get_law_v1_law__abbrev__get","parameters":[{"name":"abbrev","in":"path","required":true,"schema":{"type":"string","title":"Abbrev"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/norm/{abbrev}/{ref}":{"get":{"summary":"Get Norm","operationId":"get_norm_v1_norm__abbrev___ref__get","parameters":[{"name":"abbrev","in":"path","required":true,"schema":{"type":"string","title":"Abbrev"}},{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}},{"name":"asof","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asof"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/norm/{abbrev}/{ref}/citing-cases":{"get":{"summary":"Citing Cases","description":"Decisions that cite this provision, most significant first.\n\nEvery court is in scope — Bundesgerichte, Obergerichte and Instanzgerichte alike,\nplus the Court of Justice where it cites EU law. The ranking is: how often the\ndeciding text cites the provision, then the court's tier, then recency.\n\n**Parameters**\n\n  limit   rows per page, 1-100, default 10.\n  offset  rows to skip, for reading past the first page. `total` is the full\n          count, so `offset` up to `total` walks every citing decision.\n  sort    `weight` (default, the ranking above) or `recent`, newest decision\n          first. The default response is byte-identical to what it was before\n          this parameter existed.\n\n**`sort=recent` is BOUNDED, and says so when the bound bites.** Ordering a norm's\ncitation edges by decision date has no index to walk, so the planner sorts every\nedge; past ~20.000 edges it flips to a sequential scan of the whole `caselaw`\ntable with a ~45 MB external merge. The norm page has run this query under a\nciter-count gate and a statement timeout since and this reuses both\n(`web._NORM_RECENT_MAX_CITERS`, `web._NORM_RECENT_TIMEOUT_MS`) — 99,95 % of cited\nnorms are inside the gate; the procedural giants (§ 154 VwGO, § 708 ZPO) are not.\nOutside it, or on a timeout, the response falls back to the weight ranking and\nSAYS SO in `ranking` — it never serves a weight-ranked list under a label that\nclaims recency.\n\n**Response**\n\n  norm      the provision, as \"§ 288 BGB\"\n  total     how many citing decisions there are in all\n  offset    the offset these rows start at, echoed back\n  limit     the page size in force\n  cases     the page: court, decided, aktenzeichen, ecli, doknr, slug, url,\n            `citations` (how often this decision cites the provision) and the\n            first 300 characters of its Leitsatz\n  ranking   present ONLY when a `sort` was asked for: `sort` is the ordering\n            actually served, and `requested_sort`/`sort_note` appear when that is\n            not the one asked for\n  coverage  present ONLY when `total` is 0 for a jurisdiction whose statutes the\n            citation index does not reach — see below\n\n**A zero that is not a zero.** The citation index links decisions to FEDERAL and EU\nprovisions. For a Land statute there are no links at all, so `total: 0` used to be\nindistinguishable from \"no court has ever cited this\" — and an agent researching\nSaxon or Brandenburg law read it as a finding and wrote it down. Such a response now\ncarries `coverage: {\"state\": \"none-for-jurisdiction\", …}` naming the Land and\npointing at the case search, where that Land's decisions really are.\n\nRows are also filtered to decisions that have an addressable page here, so `total`\nnever promises a link `cases` cannot contain.","operationId":"citing_cases_v1_norm__abbrev___ref__citing_cases_get","parameters":[{"name":"abbrev","in":"path","required":true,"schema":{"type":"string","title":"Abbrev"}},{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","maxLength":20,"default":"weight","title":"Sort"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/preview/{abbrev}/{ref}":{"get":{"summary":"Preview","description":"The hover card of a citation: the norm's head and the first 300 characters.\n\n**`abs`, `satz`, `nr`, `lit`** (optional, cite-anchor 03.10.2026): the cited\nAbsatz, Satz, Nummer and Buchstabe (`§ 309 Nr. 7 Buchst. a BGB` is `nr=7&lit=a`).\nWith any of them, `text` opens at the cited Absatz and `mark` is `[start, end]` of\nthe cited Satz, Nummer or Buchstabe inside `text`; `found` lists the units that\nwere found in the stored text (a unit that is not found is not guessed). Without\nthem the answer is exactly what it has always been, and carries neither field.\n\n**`asof`** (optional, YYYY-MM-DD): the version in force on that day, as\n`/v1/norm?asof=` picks it; `version.valid_from` and `version.valid_to` are its\ndates and `asof` echoes the day. No version held for that day is a 404 that says\nwhen the archive starts, never today's text in its place. Without `asof` the\nanswer is unchanged and carries no `asof` field.","operationId":"preview_v1_preview__abbrev___ref__get","parameters":[{"name":"abbrev","in":"path","required":true,"schema":{"type":"string","title":"Abbrev"}},{"name":"ref","in":"path","required":true,"schema":{"type":"string","title":"Ref"}},{"name":"abs","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":4,"pattern":"^\\d{1,3}[a-z]?$"},{"type":"null"}],"title":"Abs"}},{"name":"satz","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":4,"pattern":"^\\d{1,3}[a-z]?$"},{"type":"null"}],"title":"Satz"}},{"name":"nr","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":4,"pattern":"^\\d{1,3}[a-z]?$"},{"type":"null"}],"title":"Nr"}},{"name":"lit","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":1,"pattern":"^[a-z]$"},{"type":"null"}],"title":"Lit"}},{"name":"asof","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":10,"pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"title":"Asof"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/preview/case/{court}/{decided}/{slug}":{"get":{"summary":"Preview Case","description":"The hover card of a decision citation that names a Randnummer (cite-anchor,\n03.10.2026): court, Aktenzeichen and the first 300 characters of Randnummer `rn`.\n\nThe path is the decision page's own (`/rechtsprechung/{court}/{decided}/{slug}`).\n`rn` is the Randnummer the decision page shows (`render.case_rn_texts`, the rows\nthat carry an `rn-N` id). A decision that shows no such Randnummer answers with its\nfirst paragraph and `rn: null`, never with a paragraph picked by position. A\nlettered `rn` the decision does not show ('15a') answers with its number ('15')\nwhen the decision shows that, as the click on the link lands there.\n`url` is the page address with the `#rn-N` fragment that lands on the row.","operationId":"preview_case_v1_preview_case__court___decided___slug__get","parameters":[{"name":"court","in":"path","required":true,"schema":{"type":"string","title":"Court"}},{"name":"decided","in":"path","required":true,"schema":{"type":"string","title":"Decided"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"rn","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":5,"pattern":"^\\d{1,4}[a-z]?$"},{"type":"null"}],"title":"Rn"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/suggest":{"get":{"summary":"Suggest Api","description":"Typeahead: citation fast path + law-alias prefix + norm-HEADING prefix FTS,\ntrigram typo fallback when all of those miss.\nNever touches full-text search or ts_headline — /v1/search at 45s+ on the small\nprod tier made the suggest dropdown unusable.","operationId":"suggest_api_v1_suggest_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":80,"title":"Q"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/case":{"get":{"summary":"Get Case By Query","description":"One decision, addressed by QUERY parameter.\n\nSame body as `/v1/case/{doknr}` — the path form cannot be built for a key that\ncontains a slash without relying on the caller, every proxy in between and the\nframework agreeing on how a percent-encoded one survives, and they do not.\nA query parameter has no such ambiguity, so this is the form the site links to\nfor those rows (`web.case_api_path`).\n\nCarries the same `status` field and the same known-missing state as\n`/v1/case/{doknr}` — see that endpoint's description.","operationId":"get_case_by_query_v1_case_get","parameters":[{"name":"doknr","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":400,"description":"A juris doknr, an ECLI or a CELEX alias. Use this form for the 54,271 doknrs that contain a slash.","title":"Doknr"},"description":"A juris doknr, an ECLI or a CELEX alias. Use this form for the 54,271 doknrs that contain a slash."},{"name":"paragraphs","in":"query","required":false,"schema":{"type":"boolean","description":"Return the decision's Randnummer paragraphs as data. Adds `paragraphs[]` and `anchor_basis`; the rest of the response is unchanged.","default":false,"title":"Paragraphs"},"description":"Return the decision's Randnummer paragraphs as data. Adds `paragraphs[]` and `anchor_basis`; the rest of the response is unchanged."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/case/{doknr}":{"get":{"summary":"Get Case","description":"One decision by juris doknr or ECLI, or by a dedup-merged CELEX alias.\n\n: `:path`, so the segment may contain slashes. Every BGH doknr does\n('BGH-Zivilsenate/XI_ZS/2001/XI_ZR_338-01A'), and until this converter went on\nthe route each one 404'd in EVERY encoding — Starlette normalises `%2F` back to\n`/` before routing, so the escaped form failed exactly like the raw one. Both\nnow resolve, and `/v1/case?doknr=…` is the third, unambiguous way in.\n\n**`status`** — every response carries it, and there are three values.\n\n`\"available\"` is a decision we hold: the body is what it has always been, with\nthe full text and every metadata field.\n\n`\"known_missing\"` is a decision we can prove EXISTS but whose text we do not\nhave. This corpus is not complete, and the honest answer for a decision that\nseveral decisions we do hold cite by Aktenzeichen is not 404 — a 404 says \"no\nsuch decision\", which for this set is false. The key for such a lookup is the\nAktenzeichen. The body is:\n\n    {\"status\": \"known_missing\",\n     \"aktenzeichen\": \"VI ZR 1/19\",\n     \"court_hint\": \"BGH\",            # INFERRED from how the citations name it\n     \"decided_hint\": \"2020-05-12\",   # INFERRED, may be null\n     \"text_available\": false,\n     \"sources\": [{\"source\": \"bverwg\",       #: an OFFICIAL free source\n                  \"url\": \"https://www.bverwg.de/de/100304B7AV4.03.0\",\n                  \"court\": \"BVerwG\",        # as the SOURCE states it\n                  \"decided\": \"2004-03-10\",  # as the SOURCE states it\n                  \"matched_on\": \"docket+court+date\",\n                  \"full_text_available\": true}, ...],\n     \"cited_by\": 7,\n     \"citers\": [{\"doknr\", \"court\", \"decided\", \"aktenzeichen\", \"url\"}, ...],\n     \"note\": \"Existence confirmed by 7 citing decisions; full text not yet\n              available.\"}\n\nNo text or paragraph field is present in that body — including with\n`?paragraphs=1`, which asks for the text that is precisely what is missing.\n`court_hint` and `decided_hint` are read off the citing decisions' own wording;\nnobody has checked them against the decision itself, because the decision is\nwhat we do not have. Treat them as leads, not as metadata.\n\n`sources` is the opposite kind of fact and is always present, `[]`\nwhen we have not found the decision anywhere. Each entry is an OFFICIAL, free\nsource that we matched to this docket on the docket AND the court AND — when\nthe citations supplied a date hint — a decision date within one day of it;\n`matched_on` says which of those two rules fired, and `court`/`decided` are\nwhat THAT SOURCE states, not what the citations infer. `full_text_available`\nmarks a source carrying the decision's full text rather than only a reference.\n\n`\"joined_with\"` is the third value and the one that is not a gap at\nall. The Court of Justice joins related cases and decides them in ONE judgment\nhanded down under all of their case numbers, but EUR-Lex catalogs that judgment\nunder the FIRST number only — so `C-493/10` names a decision that exists, was\npublished, and has no document of its own anywhere. We hold it, under\n`C-411/10`. The body is the `known_missing` body with `status: \"joined_with\"`\nand one field added:\n\n    {\"status\": \"joined_with\",\n     \"aktenzeichen\": \"C-493/10\",\n     \"joined_with\": {\"aktenzeichen\": \"C-411/10\",\n                     \"doknr\": \"62010CJ0411\",\n                     \"court\": \"Europäischer Gerichtshof\",\n                     \"decided\": \"2011-12-21\",\n                     \"url\": \"https://recht.nulegal.eu/rechtsprechung/eugh/...\",\n                     \"evidence\": \"In den verbundenen Rechtssachen C-411/10 und\n                                  C-493/10\"},\n     \"text_available\": false, ...}\n\n`text_available` stays false and there is still no text field: the text is the\nLEAD's, and `joined_with.url` is where to fetch it (`/v1/case/{doknr}` on that\ndoknr returns it with `status: \"available\"`). `evidence` is the lead judgment's\nown title line, quoted verbatim, so the relation can be checked rather than\ntrusted. `joined_with` is present and `null` on a `known_missing` body.\n\nAn Aktenzeichen nothing in the corpus cites still answers 404, as does any other\nunknown key. If you can supply the full text or a Fundstelle for a\n`known_missing` decision, the HTML page for it (`/rechtsprechung/az/{az}`) has\nthe contact route.","operationId":"get_case_v1_case__doknr__get","parameters":[{"name":"doknr","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":400,"title":"Doknr"}},{"name":"paragraphs","in":"query","required":false,"schema":{"type":"boolean","description":"Return the decision's Randnummer paragraphs as data. Adds `paragraphs[]` and `anchor_basis`; the rest of the response is unchanged.","default":false,"title":"Paragraphs"},"description":"Return the decision's Randnummer paragraphs as data. Adds `paragraphs[]` and `anchor_basis`; the rest of the response is unchanged."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search":{"get":{"summary":"Search","description":"Full-text search over federal statutes.\n\nReturns `{query, corrected, results[]}`. For the few queries that a curated page on\nthis site answers — \"wie viel Schmerzensgeld nach einem Unfall\", which no statute\nanswers — the response additionally carries `pages[]`: entries with\n`type: \"page\"`, `kind`, `title`, `url`, `description` and optional `stats`/`updated`. The key is absent otherwise, and `results` is unaffected, so a client that\nignores it is correct. Same match as the hint shown above the results on `/suche`.\n\n**This endpoint searches STATUTES. Decisions live at `/v1/case-search`** — and since\nagent-sim 15.09.2026 every response says so with numbers: `cases_available` is how\nmany decisions match the same query and `cases_url` is the call that returns them.\nThe measurement behind that: `q=Verzugspauschale` answered `{\"results\": []}` while\n184 decisions used the word, because the term is not in § 288's text (the statute\nsays \"Pauschale in Höhe von 40 Euro\") — so an API-first agent concluded the corpus\nheld nothing on it and left. An empty `results` additionally carries `suggestions`,\nthe `/v1/suggest` list, rather than being a bare dead end.\n\n`cases_available` is `null` when the count could not be measured, which is NOT zero;\nit is capped (`cases_available_capped: true` says so), because it is the same\nbounded probe that fills the Rechtsprechung tab header on `/suche`.\n\nAn unknown query parameter is a **400**, not a silent drop.","operationId":"search_v1_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2000,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"m","in":"query","required":false,"schema":{"type":"string","default":"auto","title":"M"}},{"name":"repealed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Repealed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/case-search":{"get":{"summary":"Case Search","description":"Semantic case-law search (product integration): natural-language\nquestion in, decisions out — same hybrid pipeline as the /suche\nRechtsprechung tab (dense-first for NL questions, FTS-first for keywords).\nDense hits carry the best-matching Randnummer + snippet.\n\n**Filters and ordering** (agent-sim 15.09.2026 — all three existed on `/suche` and\nnone of them on this endpoint, and `&land=SN` was answered with a byte-identical\nunfiltered list rather than an error):\n\n  land   a Bundesland: the hub slug (`sachsen`) or its code (`SN`), plus `bund`\n         and `eu`. Narrows to the courts of that Land, the same expansion\n         `/suche?land=` makes.\n  court  one court, by the slug its pages use (`ovg-sachsen`, `bgh`, `olg-koeln`).\n         ANDs with `land`; a combination no court satisfies returns an empty list,\n         which is a true answer and not a dropped filter.\n  sort   `relevance` (default, the hybrid ranking) or `date_desc`, newest first.\n         `date_desc` runs the lexical leg only — there is no dense/rerank pass to\n         re-order — and it answers \"aktuelle Rechtsprechung zu X\", which relevance\n         ranking could not: the simulated query for recent Befristung case law led\n         with decisions from 2003 and 2004.\n\nAn unknown parameter, an unknown Land or an unknown court slug is a **400** naming\nthe valid values. A filter that is silently ignored produces a well-formed,\nplausible, wrong answer, which is the worst failure shape this API can have.\n\nIf the date sort cannot be computed inside its budget the response falls back to\nrelevance and SAYS SO in `ranking.sort` — it never serves a relevance list under a\nlabel that claims recency. `ranking.sort` appears only when an ordering was asked\nfor: a default request is byte-identical to what it was before the parameter\nexisted.\n\n**`doknr` on every row** ( item 5). Purely additive — no field is removed\nor renamed. A row used to be reachable only by its `url`, so a caller chaining\nsearch into `/v1/case` had to parse our URL back into a key; `doknr` is that key\ndirectly. Null where the leg that produced the row cannot supply one, never\ninvented.","operationId":"case_search_v1_case_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2000,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":10,"title":"Limit"}},{"name":"land","in":"query","required":false,"schema":{"type":"string","maxLength":40,"default":"","title":"Land"}},{"name":"court","in":"query","required":false,"schema":{"type":"string","maxLength":80,"default":"","title":"Court"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","maxLength":20,"default":"relevance","title":"Sort"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/states":{"get":{"summary":"States Api","description":"Coverage per German state (Land): statutes and decisions held, and the binding\ngazette.\n\n`states` holds one row per Land, with `laws`, `decisions`, and `fulltext` — whether\nthis Land's statutes are held in full text. The Länder where they are also carry\n`gazette` (the binding Gesetz- und Verordnungsblatt) and `official_documentation`;\nfor the others those keys are absent, because we consolidate nothing there and\nnaming a gazette we do not read would be an invented claim.\n\nEvery number comes from the corpus itself, so this endpoint cannot go stale against\nthe thing it describes.\n\nDeprecated keys: `laender` and `fulltext_laender` carry the same values as `states`\nand `fulltext_states` and are kept only for callers written against the old names.","operationId":"states_api_v1_states_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/topics":{"get":{"summary":"Topics Api","description":"The topic axis: each Rechtsgebiet with how many decisions the corpus holds for it.\n\nThe same registry `/themen` renders, in the same order, each with the reader hub's\nURL so a caller can go from a topic to its page in one hop.\n\nDeprecated key: `themen` carries the same list as `topics` and is kept only for\ncallers written against the old name.","operationId":"topics_api_v1_topics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/stats":{"get":{"summary":"Stats Totals","description":"Corpus totals from the nightly snapshot: `laws`, `norms`, `versions`,\n`decisions`, `courts`, the two citation-graph sizes, `sources`, `years` and the\npublished ISO `weeks`, dated by `as_of` and `built_at`. `missing_case_numbers`\n(`total`, `linked`, `with_fulltext`) appears once the nightly has computed it, and\n`research` (`daily_capacity`, `remaining_today`, `queued`, `waitlisted`) only while\ndeep research is switched on.\n\nDeprecated keys: `stand`, `erstellt`, `gesetze`, `normen`, `fassungen`,\n`entscheidungen`, `gerichte`, `verweise_entscheidung_norm`,\n`verweise_entscheidung_entscheidung`, `quellen`, `jahre`, `wochen`, `recherche`\nand `vermisste_aktenzeichen` carry the same values as their English twins (the two\nblocks with their German inner keys) and are kept only for callers written\nagainst the old names.","operationId":"stats_totals_v1_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/stats/week/{iso}":{"get":{"summary":"Stats Week","description":"One ISO week ('2026-W30'). Serves weeks below the publication threshold too —\nthe threshold is about not asking Google to index a nine-row page, not about\nwithholding a number that exists.\n\n`week`, `week_start`, `week_end`, `built_at`, `new_decisions`,\n`backfilled_decisions` and `median_lag_days`, then the week's tables.\n\nDeprecated keys: `woche`, `von`, `bis`, `erstellt`, `neu`,\n`altbestand_eingespielt` and `median_verzug_tage` carry the same values and are\nkept only for callers written against the old names.","operationId":"stats_week_v1_stats_week__iso__get","parameters":[{"name":"iso","in":"path","required":true,"schema":{"type":"string","title":"Iso"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/research":{"post":{"summary":"Research Submit","description":"Queue a deep-research request; the result arrives by email.\n\n404 while LAWDB_DEEP_RESEARCH is off, which is the default — the feature is\ncommitted dark. 400 on validation, 429 on a rate cap.","operationId":"research_submit_v1_research_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/report-missing":{"post":{"summary":"Report Missing Route","description":"Report an Aktenzeichen that is real but not in lawdb.\n\nBork's boundary, verbatim in the ticket: consumers REPORT, lawdb decides\neverything else. The whole contract is: authenticated, idempotent per\n(aktenzeichen, source), answer `accepted` or `duplicate`. What happens to a\nreport afterwards (citation-graph attestation, the resolver,\ningest) is service-internal and deliberately unpromised.\n\n401 for the unkeyed/unknown-key/unresolvable-key trio alike — apikeys.\nidentify's own rule, so `nlk_` prefixes cannot be enumerated here.","operationId":"report_missing_route_v1_report_missing_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportMissingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/resolve":{"post":{"summary":"Resolve","description":"Resolve a batch of citations — norms AND decisions — or say honestly why not.\n\n added the decision half. Until it, an Aktenzeichen or an ECLI came back\n`unsupported`, so the Open Assistant's citation gate could existence-check the\nstatutes a model cited and nothing else: decisions were shown to the reader\nwithout a verified check mark even when the corpus held them.\n\nTHE SHAPE IS ADDITIVE. Every key an existing consumer reads is unchanged and\nevery norm citation resolves exactly as before (`test_resolve_cases_1001.py`\npins both). New keys:\n\n  citation_kind  norm | ecli | docket | fundstelle — which sort of identifier we\n                 read the input as. Named `citation_kind` and not `kind` because\n                 a norm result's `kind` is already the norm's own ref kind\n                 ('para'/'art'); this is the same collision `law_status` was\n                 given a distinct name for in, resolved the same way.\n  case           the decision, on a resolved decision citation\n  preview        its Leitsatz or opening text, ODbL notice attached\n  candidates     on `ambiguous` — every decision the citation can mean\n  reason/note    on a miss: the machine-readable class and the human sentence\n\n/1016/1029 add four more, all on a resolved NORM result, all additive:\n\n  subdivisions   the subdivision chain as written, values as lists — see\n                 `_resolved_norm`. `sub` is unchanged next to it.\n  sub_text       the FULL text of the cited Absätze, never truncated\n  text_truncated whether `text` is the 300-character stub or the whole thing\n  norms          on an enumeration ('§§ 4, 7 KSchG'), every member, with\n                 `enumeration` naming the refs and any the corpus lacks\n\n…plus one request field, `include`: `text` swaps a norm's stub for\nits full text, `leitsatz` swaps a decision's 294-character preview for the\nwhole Leitsatz. Both default OFF, so a caller that sends what it always sent\nreceives what it always received.\n\n`reason` is the agent API's ResolveResult enum verbatim\n(not_in_corpus / outside_coverage / unparseable / ambiguous), so the specced\n`/agent/v1/resolve` is a re-shape of this payload rather than a second opinion\nabout the same corpus.\n\n**`status: \"attested\"`** is the fifth value and the newest. It means:\nthe corpus does NOT hold this Aktenzeichen, and decisions the corpus DOES hold\nprove it exists anyway by citing it — \"echt, Text nicht geführt\". Before this\nkey such a docket answered `not_in_corpus`, which an agent verifying a citation\nreads as \"invented\", and for the up to 204,384 dockets our own decisions cite\ntwice or more that reading is wrong. (\"Up to\": that count is the nightly\nrollup's, and the answer is decided on the live citers still addressable now,\nwhich is the smaller number.)\n\nIt is a kind of MISS: there is no `case` and no `preview`, because no decision is\nbeing handed over, and `reason` stays `not_in_corpus`, because the corpus really\nwas asked and really does not have it. A consumer branching only on `reason`\ntherefore behaves exactly as it did. What rides along is the evidence:\n\n    {\"status\": \"attested\",\n     \"citation_kind\": \"docket\",\n     \"reason\": \"not_in_corpus\",\n     \"citers\": 7,                    # decisions of ours citing it, LIVE count\n     \"court_hint\": \"BGH\",            # INFERRED from how the citations name it\n     \"decided_hint\": \"1995-11-08\",   # INFERRED, may be null\n     \"attested_by\": [{\"doknr\", \"court\", \"decided\", \"aktenzeichen\", \"url\"}, …],\n     \"sources\": [{\"source\", \"url\", \"court\", \"decided\", \"matched_on\",\n                  \"full_text_available\"}, …],\n     \"note\": \"Kein Volltext … die Existenz … ist durch 7 zitierende\n              Entscheidungen des Bestands belegt.\"}\n\n`attested_by` is at most five citing decisions, each addressable, so the claim\ncan be checked rather than believed; `citers` is the true total beside it. The\nhints are read off citation prose and are leads, not metadata — the same field\nwith the same caveat `/v1/case`'s `known_missing` body carries, and the same\nrecord behind it. The threshold is two citers: one mention shares one text's\ntypos, two independent ones are evidence about the world.\n\n`sources` is `/v1/case`'s field of the same name and the same rows:\nthe OFFICIAL, free sources we matched to this docket, `full_text_available`\nmarking the ones that carry the decision's text. `[]` when we have found it\nnowhere. Saying \"we do not hold this text\" while knowing where it is published\nwould be a worse answer than the one this status replaced.\n\nA DOCKET DECIDED INSIDE A JOINED JUDGMENT IS NOT ATTESTED, IT IS RESOLVED. The\nCourt of Justice hands one judgment down under several case numbers and EUR-Lex\ncatalogs it under the first, so `C-403/01` names a decision we serve — under\n`C-397/01`. Those come back `resolved` with `matched_by: \"joined_case\"`, the\nlead's own `case` payload, and a `joined_with` object naming the lead and\nquoting the evidence for the relation. That is the same answer `/v1/case` gives\n(as `status: \"joined_with\"`) and the same answer already gives for the\njoined dockets stored as comma lists.\n\nAktenzeichen only. `case_missing` is keyed by docket, so an ECLI and a\nFundstelle have nothing to look up and those branches are unchanged.\n\nA decision citation is classified BEFORE the norm parser runs — see\n`caseresolve.classify` for why that order is the safe one and not the risky one.\n\n**`status` values** of a result: `resolved` (the identifier names one object; `case` or the norm payload is present), `ambiguous` (more than one; `candidates`), `attested` (the decision provably exists, its text is not held), `unknown` (we hold nothing under it; `reason` is `not_in_corpus` or `outside_coverage`, never a verdict that the citation is invented), `unsupported` (the string is not a citation we read; `reason: unparseable`, or a Fundstelle class we cannot look up) and, with the mismatch switch on, `mismatch` (the docket is held by a different decision than the one cited).","operationId":"resolve_v1_resolve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/linkify":{"post":{"summary":"Linkify Api","description":"Linkify: send `html` (fragment) or `text` (plain),\nreceive it back with recognized citations linked to recht.nulegal.eu.\n\nContract (documented on /vernetzung):\n  * everything outside inserted <a class=\"nulegal-cite\"> is byte-identical\n  * norms link only when explicitly qualified AND in the corpus; case\n    citations only when the Aktenzeichen is unique in the corpus — the\n    response never contains a link that would 404\n  * datenschutz-neutral: content is processed in memory and not stored;\n    no cookies, no tracking. Callers SHOULD cache per content hash —\n    responses are deterministic per corpus state, so long local caching\n    is safe.\n413 over ~300k characters, 429 over the per-IP rate (Retry-After: 60).","operationId":"linkify_api_v1_linkify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LinkifyRequest":{"properties":{"html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"}},"type":"object","title":"LinkifyRequest"},"ReportMissingRequest":{"properties":{"aktenzeichen":{"type":"string","maxLength":120,"minLength":1,"title":"Aktenzeichen"},"court":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Court","description":"The deciding court, if known. Deprecated spelling: `gericht`, still accepted."},"decided":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Decided","description":"The decision date, if known. Deprecated spelling: `datum`, still accepted."},"evidence_urls":{"items":{"type":"string","maxLength":500},"type":"array","maxItems":10,"title":"Evidence Urls","description":"Up to 10 http(s) URLs where the decision is attested. Deprecated spelling: `beleg_urls`, still accepted."},"source":{"type":"string","maxLength":32,"minLength":1,"title":"Source","description":"Who is reporting, e.g. `open-assistant`; one report per (aktenzeichen, source). Deprecated spelling: `quelle`, still accepted."},"context":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Context","description":"Where the Aktenzeichen was encountered. Deprecated spelling: `kontext`, still accepted."}},"type":"object","required":["aktenzeichen","source"],"title":"ReportMissingRequest","description":" — one report of a real-but-absent Aktenzeichen. Bounds mirror\n`report_missing.validate` (the module re-checks; the model is the 422 face).\n\nEnglish field names. `gericht`, `datum`, `beleg_urls`, `quelle` and\n`kontext` are still accepted, and the English name wins when both are sent, even\nwhen its value is null. A 422 for a missing field names the English field.\n`aktenzeichen` stays: it is the term of art, the same word every /v1 payload uses."},"ResearchRequest":{"properties":{"question":{"type":"string","maxLength":2000,"minLength":1,"title":"Question"},"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"}},"type":"object","required":["question","email"],"title":"ResearchRequest"},"ResolveRequest":{"properties":{"citations":{"items":{"type":"string","maxLength":200},"type":"array","maxItems":100,"title":"Citations"},"include":{"items":{"type":"string","enum":["text","leitsatz"]},"type":"array","maxItems":4,"title":"Include"},"text":{"type":"string","maxLength":20000,"title":"Text","description":"Running text instead of `citations`: every norm citation, docket, EU case number and ECLI in it is found and resolved, one result each with its `span` [start, end]. `unparsed` lists the remaining stretches that still contain a digit. At most 100 citations are resolved; `truncated: true` says the text held more (and `unparsed` is then empty). Read only when `citations` is absent."}},"type":"object","required":[],"title":"ResolveRequest","description":"Send `citations` (a list of citation strings) or `text` (running text). An entry of `citations` that holds several citations keeps its own result and lists the others under `extracted` (at most {resolve_text.MAX_FOUND} per request; `extracted_truncated: true` past that)."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}