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
curl "https://recht.nulegal.eu/v1/search?q=k%C3%BCndigung&limit=3"

Basics

API basics
Base URLhttps://recht.nulegal.eu
AuthNone. No key or token required.
FormatJSON (UTF-8). Query strings must be URL-encoded.
CORSOpen (*) for GET, POST, OPTIONS. Call it directly from the browser.
CachingResponses send Cache-Control: public, max-age=3600. Please cache on your side.
Personal dataNot 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

GET/v1/search

Search across all federal statutes. Ranks by relevance, pins curated matches for common terms and returns a plain-text snippet for each hit.

Full-text search parameters
ParameterTypeDescription
qstring, requiredSearch term. Minimum 2 characters. Accepts a keyword (kündigung), a citation (§ 823 BGB) or a natural-language question.
limitintegerNumber of results, 1 to 100. Default 20.
repealedbooleanInclude 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.
mstringSearch mode: auto (default), fast or deep.

Example

curl
curl "https://recht.nulegal.eu/v1/search?q=k%C3%BCndigung&limit=2"
JavaScript
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);
Response
{
  "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.

Response
{
  "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"
    }
  ]
}
Fields of a curated page match
FieldTypeDescription
typestringAlways page.
kindstringStable identifier of the page family, e.g. schmerzensgeldtabelle. New kinds may be added over time; treat an unknown kind as a generic page.
titlestringDisplay title, German, including the selected facet.
urlstringAbsolute URL, same convention as results[].url.
descriptionstringOne paragraph answering the query, with the figures the page shows.
statsobject, optionalMachine-readable version of those figures. Keys depend on kind.
updatedstring, optionalISO 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

GET/v1/case-search

Semantic search over court decisions. A natural-language question in, decisions out. Hits carry the best-matching paragraph number (rn) and a snippet.

Case-law search parameters
ParameterTypeDescription
qstring, requiredQuestion or keyword. Minimum 2 characters.
limitintegerNumber of results, 1 to 20. Default 10.
deep_forceflagSet 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
curl "https://recht.nulegal.eu/v1/case-search?q=wann%20darf%20mein%20chef%20mir%20k%C3%BCndigen&limit=2"
Response
{
  "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:

More endpoints
EndpointDescription
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-casesFederal decisions that cite a given norm.
GET /v1/suggest?q=Typeahead suggestions for laws and norms.
POST /v1/resolveResolve 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:

Full-text fields on /v1/resolve
Field or parameterWhat it gives you
sub_textAutomatic, 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:

Subdivision fields on /v1/resolve
FieldDescription
subUnchanged. The flat form: abs, satz, nr, lit, one value each — the first, where the citation named several.
subdivisionsThe 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 + enumerationOn 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
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.

Citation check
EndpointDescription
POST /agent/v1/quote/verifySend 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.

Decision permalink endpoints
EndpointDescription
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
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-21

These 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.

URL
https://recht.nulegal.eu/rechtsprechung/bgh/2023-06-26/via-zr-335-21#rd_13
https://recht.nulegal.eu/gesetze/BGB/823#abs-2

Both 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
curl "https://recht.nulegal.eu/v1/case/KORE601832026?paragraphs=1"
The paragraphs array
FieldMeaning
paragraphs[].textThe paragraph, exactly as the decision page shows it, in document order.
paragraphs[].rnThe 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[].anchorThe 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[].urlThe permalink, already built: the decision URL plus the fragment.
anchor_basisnative_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

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).