The German law API
Every federal statute and hundreds of thousands of court decisions, as clean JSON. Full-text search, semantic case search, norm lookup and citation resolution. No key, no sign-up, open CORS. Built and run by Nulegal GmbH.
Quick start
Every endpoint is a plain GET that returns JSON. Try it in your terminal:
curl "https://recht.nulegal.eu/v1/search?q=k%C3%BCndigung&limit=3"Basics
| Base URL | https://recht.nulegal.eu |
|---|---|
| Auth | None. No key or token required. |
| Format | JSON (UTF-8). Query strings must be URL-encoded. |
| CORS | Open (*) for GET, POST, OPTIONS. Call it directly from the browser. |
| Caching | Responses send Cache-Control: public, max-age=3600. Please cache on your side. |
| Personal data | Not permitted. Do not include names, case facts or other personal data in queries or request bodies, including /v1/resolve. Requests are logged. See the Terms of Use. |
Full-text search
Search across all federal statutes. Ranks by relevance, pins curated matches for common terms and returns a plain-text snippet for each hit.
| Parameter | Type | Description |
|---|---|---|
q | string, required | Search term. Minimum 2 characters. Accepts a keyword (kündigung), a citation (§ 823 BGB) or a natural-language question. |
limit | integer | Number of results, 1 to 100. Default 20. |
repealed | boolean | Include norms of repealed statutes, matched against their last known text. Default false: search returns law in force. Results are additive — everything the default search returns is still there, ranked among the repealed matches. Repealed provisions stay addressable at their own URL regardless of this parameter, and /v1/norm resolves them (and ?asof= versions) unconditionally. |
m | string | Search mode: auto (default), fast or deep. |
Example
curl "https://recht.nulegal.eu/v1/search?q=k%C3%BCndigung&limit=2"const r = await fetch(
"https://recht.nulegal.eu/v1/search?q=" + encodeURIComponent("kündigung") + "&limit=2"
);
const { results } = await r.json();
console.log(results[0].ref, results[0].jurabk, results[0].url);{
"query": "kündigung",
"results": [
{
"jurabk": "KSchG",
"slug": "kschg",
"ref": "§ 1",
"ref_norm": "1",
"heading": "Sozial ungerechtfertigte Kündigungen",
"rank": 0.48,
"snippet": "Kündigung des Arbeitsverhältnisses gegenüber einem …",
"law_url": "KSchG",
"url": "https://recht.nulegal.eu/gesetze/KSchG/1"
}
]
}jurabk ist der Schlüssel, mit dem
/v1/law/{abbrev} und /v1/norm/{abbrev}/{ref} arbeiten — er
bleibt unverändert. Wo dieser Schlüssel nicht die Zitierform ist, liefert die Antwort
zusätzlich display_abk: die Bezeichnung, die auch auf den Seiten dieser
Site steht. Das betrifft zwei Fälle — Fundstellennachweis-Kennungen des Bundesrechts
("jurabk": "AO 1977" → "display_abk": "AO",
"SGB 2" → "SGB II") und Landesrecht Brandenburg, dessen
Portal für 144 Dokumente keine Abkürzung veröffentlicht und wo der Schlüssel eine
interne Dokument-ID ist ("jurabk": "VT-ID242586" →
"display_abk": "Vertrag zwischen dem Heiligen Stuhl und dem Land
Brandenburg"). Das Feld fehlt, wenn beide gleich wären —
bei den meisten Treffern also. Für Anzeigezwecke:
r.display_abk ?? r.jurabk.
Curated pages
Some questions no single statute answers. “Wie viel Schmerzensgeld nach einem
Unfall?” is one: § 253 BGB grants a “billige Entschädigung in Geld”
and names no amount. For those queries the response carries an extra
pages array next to results — curated pages on this site
that do answer the question, each with the figures it shows, so you can answer without
a second request. results is unaffected, and pages is
absent for the vast majority of queries, so existing clients need no
change. Every entry carries "type": "page", which is how you tell one
apart from a norm hit if you merge the two lists.
{
"query": "schmerzensgeld nach unfall wie viel",
"results": [ … ],
"pages": [
{
"type": "page",
"kind": "schmerzensgeldtabelle",
"title": "Schmerzensgeldtabelle: Verkehrsunfall",
"url": "https://recht.nulegal.eu/schmerzensgeldtabelle?kontext=verkehrsunfall",
"description": "199 Entscheidungen deutscher Gerichte mit zugesprochenem Schmerzensgeld (Verkehrsunfall): Median 4.500 €, Spanne …",
"stats": { "n": 199, "median_eur": 4500.0, "min_eur": 100.0, "max_eur": 500000.0 },
"updated": "2026-07-27"
}
]
}| Field | Type | Description |
|---|---|---|
type | string | Always page. |
kind | string | Stable identifier of the page family, e.g. schmerzensgeldtabelle. New kinds may be added over time; treat an unknown kind as a generic page. |
title | string | Display title, German, including the selected facet. |
url | string | Absolute URL, same convention as results[].url. |
description | string | One paragraph answering the query, with the figures the page shows. |
stats | object, optional | Machine-readable version of those figures. Keys depend on kind. |
updated | string, optional | ISO date the underlying data was last built. |
The same match drives the hint shown above the results on /suche, so
the website and the API never disagree about which page answers a query.
Case-law search
Semantic search over court decisions. A natural-language question in, decisions out. Hits
carry the best-matching paragraph number (rn) and a snippet.
| Parameter | Type | Description |
|---|---|---|
q | string, required | Question or keyword. Minimum 2 characters. |
limit | integer | Number of results, 1 to 20. Default 10. |
deep_force | flag | Set deep_force=1 to request the LLM
re-ranking pass even when your traffic is classified as automated. The response's
ranking.llm_rerank field says what happened: attempted (the
extra LLM pass ran over the candidate pool), skipped-nonhuman-traffic
or rate-capped; the field is absent when the query shape (short keyword,
exact docket) never makes re-ranking applicable. |
Example
curl "https://recht.nulegal.eu/v1/case-search?q=wann%20darf%20mein%20chef%20mir%20k%C3%BCndigen&limit=2"{
"query": "wann darf mein chef mir kündigen",
"results": [
{
"court": "BAG",
"decided": "2011-11-24",
"aktenzeichen": "2 AZR 429/10",
"doktyp": "Urteil",
"rn": null,
"snippet": "Kündigung noch nach Fristablauf wirksam …",
"source": "rii",
"url": "https://recht.nulegal.eu/rechtsprechung/bag/2011-11-24/2-azr-429-10"
}
]
}When a result comes from an Open-Database-Licence source, the response adds an
attribution array. If present, show it alongside the results.
More endpoints
The same API also resolves single norms and citations:
| Endpoint | Description |
|---|---|
GET /v1/norm/{law}/{ref} | One norm with full text and version metadata. Optional ?asof=YYYY-MM-DD for a historical version. |
GET /v1/preview/{law}/{ref} | One norm as a short snippet. Powers the citation badge preview. |
GET /v1/law/{law} | A law with its table of norms. |
GET /v1/case/{doknr|ecli} | One decision by juris document number or ECLI, with its cited norms. Add ?paragraphs=1 for the decision's Randnummern as data (see below). |
GET /v1/case?doknr= | The same decision, addressed by query parameter. Use this form for the 54.271 document numbers that contain a slash — every BGH, BPatG and NRWE doknr does (BGH-Zivilsenate/XI_ZS/2001/XI_ZR_338-01A). |
GET /v1/norm/{law}/{ref}/citing-cases | Federal decisions that cite a given norm. |
GET /v1/suggest?q= | Typeahead suggestions for laws and norms. |
POST /v1/resolve | Resolve up to 100 raw citation strings — norms and decisions (Aktenzeichen, ECLI, BVerfGE-Fundstelle). Each result carries citation_kind and one of resolved / ambiguous / unknown / unsupported; a docket several decisions share comes back ambiguous with every candidate named, never one of them picked. A miss carries a machine-readable reason and a sentence saying why. See below for how much text a result carries. |
How much text a resolve result carries
POST /v1/resolve truncates by default. A resolved
norm’s text is the first 300 characters of the whole
provision, cut on a word boundary and ending in …, and it is the same
300 characters whichever subsection you cited. A resolved decision’s
preview is 300 characters of its Leitsatz. text_truncated
says which you got. Do not verify a quotation against a truncated
field. Three ways to get the real text:
| Field or parameter | What it gives you |
|---|---|
sub_text | Automatic, no opt-in. When the citation names an Absatz, the full text of exactly those Absätze, never truncated: {"abs": ["1","3"], "text": "(1) … (3) …"}. null when no Absatz was cited or the provision is not Absatz-numbered — never a guess about where a subsection starts. |
"include": ["text"] | Request field. The norm’s whole text in text instead of the stub; text_truncated becomes false. |
"include": ["leitsatz"] | Request field. A decision’s whole Leitsatz in a new leitsatz field. preview is unchanged next to it. null where the decision carries no Leitsatz — which is also when preview is showing the head of the text instead. |
Both include values are off by default, so a request that does not
send the field gets exactly the payload it always got. An unknown value is a
422, not a silent no-op.
Citations with more than one subsection
A citation may name several subsections, a range of them, or several
paragraphs sharing one law name — § 1374 Abs. 1 und 3 BGB,
§ 1376 Abs. 1–3 BGB, § 543 Abs. 1, Abs. 2 Satz 1 Nr. 1 und
Abs. 3 BGB, §§ 4, 7 KSchG. All of these resolve, and two
fields carry what was written:
| Field | Description |
|---|---|
sub | Unchanged. The flat form: abs, satz, nr, lit, one value each — the first, where the citation named several. |
subdivisions | The chain as written, in order, values as lists: [{"key":"abs","values":["1","3"]}]. Order matters — it is the only record of which Satz belongs to which Absatz. A written range is expanded to its members. Keys: abs, satz, nr, ziff, lit, halbs, alt, var, uabs, teil, anlage, anhang, abschnitt. |
norms + enumeration | On an enumeration (§§ 4, 7 KSchG): every member, each with its own sub_text. The top-level result stays the first member, so nothing you already read changes. A member the corpus does not hold is named in enumeration.unresolved rather than dropped. |
A §§-range (§§ 305–310 BGB) keeps its existing shape:
range plus every norm in between under norms.
curl -s -X POST "https://recht.nulegal.eu/v1/resolve" \
-H "content-type: application/json" \
-d '{"citations": ["§ 1374 Abs. 1 und 3 BGB", "ECLI:DE:BGH:2013:201113UXIIZR77.12.0"],
"include": ["leitsatz"]}'A repealed statute, or a provision that was removed from one, has no text in
force. /v1/norm, /v1/preview and /v1/resolve
then answer with the last known text rather than an empty one, and
label it: status is repealed or in_force (the
law's own status, as /v1/law reports it), text_status is
current or historical, and version.valid_to
carries the date the text stopped being observed. Treat
text_status: "historical" as "this is not the law in force today".
?asof= is unaffected: a date after the repeal still answers a
404, because nothing applied then.
An interactive reference with every parameter is at /docs (OpenAPI).
Citation check (preview)
Not yet enabled in production. This endpoint is built and
tested but switched off on the live service; it is documented here so that
anyone building against the API can see the contract before it goes live. It is
the first operation of the agent API — the paid, keyed surface described in
drafts/agent-api/DESIGN.md. Everything above it on this page stays
free and unchanged.
| Endpoint | Description |
|---|---|
POST /agent/v1/quote/verify | Send a statement (the sentence that cites) and a citation (a norm citation, an Aktenzeichen, an ECLI, a Fundstelle). Get back supported, not_supported, not_found or undetermined, plus the passage it was decided on. Optional question adds a relevant verdict about the user's own question. |
Two layers answer it. An exact or near-exact quotation is settled by string
matching, with no model involved — that answer carries
basis: "containment". A paraphrase is settled by a verifier model
reading the resolved source text, and carries basis: "model". The
string-match verdict is reported either way under containment
(verbatim / near / absent).
A citation with a subdivision is read as written: § 823 Abs. 2 BGB
is verified against Absatz 2 and nothing else, and a subdivision the norm does not
have answers not_found rather than a verdict about a different
Absatz.
undetermined is a real answer and needs handling: it means the
check could not be completed — not that the statement is unsupported.
reason says which (model_rate_capped,
model_unavailable, model_declined_source,
source_text_unavailable, source_incomplete,
historical_version_unsupported). Treating it as a failed check would
report a correct citation as a fabrication.
Two of those are worth knowing about in advance. Some provisions are held with
no text at all — a number of EU regulation articles among them — and a
citation to one answers source_text_unavailable rather than a verdict
about an empty string. And a citation that names another version
(§ 306 BGB a.F.) is declined rather than checked against the text in
force, which is a different provision: ask
/v1/norm/{law}/{ref}?asof=YYYY-MM-DD for a Fassung.
curl -sX POST https://recht.nulegal.eu/agent/v1/quote/verify \
-H 'Content-Type: application/json' \
-d '{"statement":"Eine ohne Anhörung des Betriebsrats ausgesprochene Kündigung ist unwirksam.",
"citation":"§ 102 Abs. 1 BetrVG"}'
An Authorization: Bearer key is optional. Without one the call is
served and graded on ordinary traffic classification; automated traffic gets the
string-match answer and undetermined where a model would have been
needed. Keys, quotas and pricing are not open for sign-up yet — write to
kontakt@nulegal.de if you want to be told
when they are.
Link to a decision (permalinks)
Want to link to a court decision from your site, newsletter or app without knowing our
URLs? Build a link from what a citation already gives you — court, date, docket number
(Aktenzeichen). A unique match answers with a 301 redirect to the decision page;
if several decisions share the docket number you get a small picker page; if we do not have
the decision you get a 404 with pointers to external sources.
| Endpoint | Description |
|---|---|
GET /finde?gericht=&datum=&az= | Resolver with three
optional parts. gericht takes full names or common abbreviations
(BGH, OLG Köln, VG Berlin, EuGH),
any casing. datum takes 2023-06-26 or 26.06.2023.
az is the docket number, any of the German or European formats. Give as much
as you have — the docket number alone is often enough. Try it:
a worked example. |
GET /az/{aktenzeichen} | Short form when the docket number is
all you have, e.g. /az/VI ZR 496/15 or /az/C-311/18. |
GET /rechtsprechung/bverfge/{band}-{seite} | Resolve a
BVerfGE citation from the court’s amtliche Sammlung: /rechtsprechung/bverfge/30-173
for BVerfGE 30, 173. The pin-cite form works too
(/rechtsprechung/bverfge/30-173-177 for “BVerfGE 30, 173 (177)”) and redirects to the
canonical address first. Where the Sammlung prints more than one decision at a page, the citation
gets a picker page instead of a guessed redirect. |
Example
curl -I "https://recht.nulegal.eu/finde?gericht=BGH&datum=26.06.2023&az=VIa%20ZR%20335%2F21"
# HTTP/1.1 301 Moved Permanently
# location: /rechtsprechung/bgh/2023-06-26/via-zr-335-21These are HTML endpoints meant for links, not JSON. All of them are stable: a link that resolves today keeps resolving.
Deep links to a Randnummer or Absatz
Decision pages carry a permanent anchor per paragraph (Randnummer): append
#rd_N to the decision URL. Norm pages carry one per numbered subsection
(Absatz): append #abs-N to the norm URL. The page highlights the targeted
passage and shows its permalink right there, ready to copy.
https://recht.nulegal.eu/rechtsprechung/bgh/2023-06-26/via-zr-335-21#rd_13
https://recht.nulegal.eu/gesetze/BGB/823#abs-2Both fragment formats are permanent, under the same discipline as the resolver
endpoints above. Decision texts are immutable, so a #rd_N link keeps pointing
at the same passage forever. Norm texts follow the legislature: #abs-N always
resolves to paragraph N of the current version, which an amendment may renumber or repeal,
so cite the passage in text alongside the link where permanence matters.
/v1/case-search already returns the best-matching Randnummer per hit
(rn) and a ready deep link in url. The full stability promise,
in German, is documented at /vernetzung.
Randnummern as data
A pinpoint citation names a paragraph — “BGH, VIII ZR 67/18 Rn. 27” — and
recovering Rn. 27 from a 40.000-character text_plain by regex is how
integrations end up citing the wrong one. So the endpoint hands you the paragraphs the
decision page itself renders. GET /v1/case/{doknr}?paragraphs=1 adds two keys
and changes nothing else; without the parameter the response is exactly what it always
was.
curl "https://recht.nulegal.eu/v1/case/KORE601832026?paragraphs=1"| Field | Meaning |
|---|---|
paragraphs[].text | The paragraph, exactly as the decision page shows it, in document order. |
paragraphs[].rn | The Randnummer the court printed for this paragraph, as a string — or null. Never inferred from position: null means this decision prints no number here, and you must not present the anchor as an “Rn.” instead. |
paragraphs[].anchor | The permalink fragment of this paragraph on the decision page (rd_27), or null where the page anchors it either. Permanent, under the promise above. |
paragraphs[].url | The permalink, already built: the decision URL plus the fragment. |
anchor_basis | native_numbering — the anchors are the printed Randnummern, so #rd_27 is Rn. 27. document_order — the anchors are our own 1-based index over the addressable paragraphs and nothing more. Measured per decision, not assumed per court. |
Read anchor_basis before citing. It is native_numbering for the
juris decision-XML corpus, where both numbers come out of the same field — that is the BGH,
BVerfG, BAG, BVerwG, BSG and BFH text served from rechtsprechung-im-internet.de. It is
document_order elsewhere, and there the two numbers really do differ: in EuGH
judgments our index runs one ahead of the printed number for the whole judgment, because the
title block is a paragraph too. Where a decision prints no paragraph numbers at all — much of
the older BGH text, which reaches us as extracted PDF — every rn is
null and the paragraphs are still returned in document order, so you can quote a
passage without being able to name a Randnummer for it.
Usage and fair use
- The API is free. There is no key and no hard rate limit today. Please stay reasonable and cache responses; heavy or automated bulk use may be throttled.
- Text and data mining is expressly permitted. The pages and the API are open to
AI assistants: our robots.txt names the admitted AI agents
and opens
/v1to them at the crawl delay it states, and /llms.txt documents the same rule for machine readers. - One condition: automated access must not present itself as a human visitor. Use an accurate user agent that identifies your operator or includes a contact route, and do not circumvent technical access restrictions. Details in the Terms of Use.
- Statute text is public domain under § 5 UrhG. Court decisions carry the licence of their
source; where an
attributionfield is present, keep it with the data. - All texts are consolidated, non-official reading versions. The binding source is the Federal Law Gazette at recht.bund.de.
- Building something with it? We would like to hear about it: reach us via nu:legal.
Looking to embed citations on your own site instead? See the citation badge — or let Vernetzung link every legal citation in your pages automatically (API, WordPress plugin, or a one-line script tag).