kvasir.legal

将您的法律AI建立在可验证的德国、奥地利、瑞士及欧盟法律之上——通过来源追溯和精准深度链接解析、搜索和验证法律引用。涵盖44万+条法规,支持在Claude和ChatGPT中运行交互式应用。

文档

Verifiable lawfor your AI.

German, Austrian, Swiss and EU law as one canonical object per legal entity — norms, statutes, and case law for the German federal courts, Bavaria, NRW, the Austrian supreme courts and the CJEU — with provenance, sub-units addressable down to the sentence, ready-made citations and the citation graph. For the key EU digital acts: delegated & implementing acts and Commission guidelines included, linked to their base act. Model-agnostic: call it over REST, or plug it into any agent as an MCP tool. You bring the reasoning; we bring the ground truth.

Base URL https://kvasir.legal · questions? [email protected] · new to grounding? Why legal AI needs a grounding layer → · how we measure it: the Grounding Benchmark →

Getting access. Free, no invite needed. Create an account → then confirm your email address, and the dashboard (keys, usage, rotation) is yours. Already signed up? Create a key in your dashboard → No key needed to try the sandbox (20 requests/day).

Building something bigger? Tell me what you need — coverage, volume, endpoints. Design partners lock founding conditions.

Connect in 60 seconds

One endpoint — https://kvasir.legal/mcp — plugs kvasir into the AI you already work with. Your assistant then searches, resolves and verifies citations against the live corpus instead of answering from memory.

Just want to try it? https://kvasir.legal/mcp/demo — the same seven tools, no key, no signup: 10 tool calls per day and IP. Add it as a custom connector (or claude mcp add --transport http kvasir-demo https://kvasir.legal/mcp/demo) and ask your assistant to check a draft. Use the main /mcp endpoint for real work — it carries your key or OAuth sign-in and your own limits.

ChatGPT. Settings → Connectors → Advanced → Developer mode → add MCP server https://kvasir.legal/mcp with header X-API-Key → enable it in your chat's tools.

Claude — no key needed. Settings → Connectors → Add custom connector → paste https://kvasir.legal/mcp → sign in with your kvasir account → done. All seven tools appear in every chat.

Microsoft 365 Copilot. Available firm-wide via Copilot Studio: your IT admin adds https://kvasir.legal/mcp once as a custom MCP action (API key as X-API-Key) — every lawyer in the tenant can then use it in Copilot chats.

Perplexity. On plans with connector support: Settings → Connectors → add an MCP connector with URL https://kvasir.legal/mcp and your X-API-Key.

Le Chat (Mistral). Settings → Connectors → add MCP connector → URL https://kvasir.legal/mcp + X-API-Key.

Developer tools & agents. Any MCP-capable client speaks to https://kvasir.legal/mcp (streamable-HTTP) with your key as X-API-Key — or use plain REST (interactive reference).

Claude Code (one command):

claude mcp add --transport http kvasir https://kvasir.legal/mcp --header "X-API-Key: kvk_…"

Claude Desktop (claude_desktop_config.json, via mcp-remote):

{
  "mcpServers": {
    "kvasir": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://kvasir.legal/mcp",
               "--header", "X-API-Key: kvk_…"]
    }
  }
}

Cursor / VS Code (.cursor/mcp.json · .vscode/mcp.json):

{
  "mcpServers": {
    "kvasir": {
      "url": "https://kvasir.legal/mcp",
      "headers": { "X-API-Key": "kvk_…" }
    }
  }
}

Authentication

Try it without a key: resolve, search and object work keyless in sandbox mode — 20 requests per day, no signup. Just call them:

curl --get "https://kvasir.legal/api/v1/resolve?include=none" --data-urlencode "cite=§ 573 Abs. 2 Nr. 2 BGB"

For real use, send an API key as a header (either form works):

X-API-Key: kvk_your_key
Authorization: Bearer kvk_your_key

Keyless calls beyond the sandbox trio (or past the daily 20) get 401 / 429 with a JSON hint. The machine-readable contract is public at /api/v1/schema (no key needed).

Connecting from Claude or another MCP client? Just add https://kvasir.legal/mcp as a connector — you'll be walked through an OAuth sign-in with your kvasir account instead of pasting a key. Keys and OAuth work side by side; both count against the same tier.

Scopes

Two scopes, split along the only line that matters — what your client transmits:

ScopeWhat it allows
legal.readEverything except draft checking. Your client sends citation strings, URIs and search queries. Always granted.
legal.checkAdds check_draft / POST /api/v1/check-draft, which transmits the full text you submit. Granted separately.

On OAuth sign-in you decide with a checkbox; the scope is bound to that connection and a token refresh never widens it. API keys carry both scopes by default — a key restricted to legal.read can be issued on request, and then draft checking is refused with 403 insufficient_scope rather than merely discouraged. That is how a connector can be provisioned so it is technically incapable of receiving client documents. See Trust & Security.

Quickstart

No key, no signup — this works right now (sandbox, 20/day):

curl --get "https://kvasir.legal/api/v1/resolve?include=none" --data-urlencode "cite=§ 573 Abs. 2 Nr. 2 BGB"

With a key, fetch the full object:

curl -H "X-API-Key: kvk_…" \
  "https://kvasir.legal/api/v1/object/norm:bund:BGB:242?include=all"
{
  "uri": "norm:bund:BGB:242",
  "canonical_uri": "norm:bund:BGB:242",
  "kind": "norm",
  "exists": true,
  "identity":   { "title": "§ 242 BGB", "subtitle": "Bundesrecht", "href": "/bgb/242" },
  "provenance": { "quelle": "bund", "source": "gesetze-im-internet.de", "derived": false, "version": "" },
  "content":    { "label": "§ 242", "plain_text": "Der Schuldner ist verpflichtet …", "html": "…" },
  "units": [
    { "path": "a1", "unit": "abs", "label": "1",
      "text": "…", "citation": "§ 242 Abs. 1 BGB", "deeplink": "/bgb/242#a1" }
  ],
  "relations": { "in": [ … cited-by … ], "out": [ … cites … ] }
}

Object URIs

Every legal object is addressed by a stable URI:

KindURIExample
Normnorm:{quelle}:{kuerzel}:{norm_id}norm:bund:BGB:242 · EU: norm:eu:GDPR:art_6
Statutegesetz:{quelle}:{kuerzel}gesetz:bund:BGB · gesetz:eu:GDPR
Decisiondec:{quelle}:{source_id|celex}dec:eu:62018CJ0311 (Schrems II, C‑311/18)
Sectionsection:{quelle}:{kuerzel}:{path_hash}from a statute's children
Conceptconcept:{scheme}:{slug}concept:eu:protection-of-personal-data
Norm versionnormversion:{id}normversion:1234
Materialmat:{quelle}:{ident}mat:eu:52022PC0454

quellebund (federal), by (Bavaria), nrw (North Rhine-Westphalia), sn (Saxony), bb (Brandenburg), hb (Bremen), at (Austria), ch (Switzerland), eu. Norm URIs are tolerant: norm:bund:BGB:242 resolves the same object as the raw slug; the response's canonical_uri gives the clean form. EU norms use art_N ids (norm:eu:GDPR:art_6, not …:6) — when unsure, go through /api/v1/resolve, which returns the canonical URI. Materials are legislative documents, always linked to the acts and provisions they found, amend or concretise. For the EU: Commission proposals (with explanatory memorandum) and Commission guidelines (typ: "guideline" — interpretive, not binding law). For Germany: pending federal bills from the Bundestag's DIP (typ: "vorhaben" — procedural stage, draft text and per-provision amendment commands) and BaFin administrative acts (typ: "verfuegung"). Drafting history, official interpretation, and an early-warning signal for pending changes.

Endpoints

Prefer clicking around? Try the interactive API reference. Building with an LLM? Point it at /llms.txt.

GET /api/v1/resolve?cite=§ 242 Abs. 1 BGB

Citation in, object out. Parses free-text citations (Art. 6 Abs. 1 lit. f DSGVO, C-311/18) — no URI grammar needed. Returns the pinpoint sub-unit and did-you-mean suggestions when something doesn't resolve.

Ranges. § 433 f. BGB and § 433 ff. BGB come back with a range, and the two are deliberately not treated alike. f. means exactly one further provision, so to is exact — and it follows the statute's own order, where § 311 f. BGB ends at § 311a, not § 312. ff. is open-ended: its end is not encoded in the citation, so kvasir returns suggested_to (the end of the innermost section around the first provision) and marks it as an inference, never as a finding. What is provable is that something follows at all — § 2385 ff. BGB yields range.valid: false, because the BGB ends there and the "ff." has nothing to refer to.

Below the provision the marker binds to the deepest level named — § 305 Abs. 1 ff. BGB means paragraphs, § 573 Abs. 2 Nr. 1 ff. BGB means numbers — and comes back as unit_range. There even "ff." is exact: the provision bounds its own list, so § 305 with three paragraphs resolves "Abs. 1 ff." to paragraphs 1–3, with every covered unit in paths. The open-endedness above exists only because a statute keeps going after § 433; a provision does not.

Several provisions in one string. §§ 433, 434 BGB, §§ 433-435 BGB, § 573 Abs. 1, 2 BGB and § 823 Abs. 1 i.V.m. § 1004 BGB all name more than one provision. Each is resolved: the first as the main object, the rest under also_cited with their own exists. Which level a continuation belongs to follows the deepest one already named — , 2 after Abs. 1 is a paragraph, after § 433 a section. Top-level exists answers for the whole citation, so one invented number in the list turns it false rather than passing on the strength of the first hit.

Variants and jurisdiction. § 263 Abs. 1 Var. 2 StGB resolves down to the paragraph and reports variant.addressable: false: Tatvarianten count the alternatives inside a provision's wording, but the statute does not number them, so there is nothing to verify against — kvasir says so instead of inventing an anchor. Separately, &quellen=at restricts the jurisdiction, which matters for abbreviations that exist more than once: § 15 StGB is German federal law by default and Austrian only if you ask for it. The filter restricts rather than ranks, so no hit means "not found in that jurisdiction".

When one abbreviation fits two jurisdictions. BV is the Swiss federal constitution and the Bavarian one; Art. 141 exists in both — Swiss referendum law here, the right to roam in the open countryside there. A resolver that just picks the higher-ranked jurisdiction answers a question it was never asked, and answers it invisibly. So when the same abbreviation carries the cited number in more than one jurisdiction, the response adds ambiguous with every candidate, and check_draft raises ambiguous_jurisdiction. The resolution still happens — the first candidate by jurisdiction priority — but it is labelled as a choice rather than a finding. Pass quellen to settle it.

Versions. § 433 BGB a.F. asks for an earlier text than the one in force. kvasir serves consolidated current law, so the answer carries version.confirmed: false: the citation is fine, the corpus just cannot evidence it. Superseded texts exist only from kvasir's own first index run and are dated by detection, not by legal effect — so "which version was in force on 1 Jan 2020" is a question this API does not answer, and says so rather than returning today's text as if it were the answer. Archived predecessors, where they exist, are listed under superseded and retrievable as normversion:<id>.

GET /api/v1/relations/<uri>?edge_type=zitiert_norm&limit=50

The case law on a provision. include=relations on an object gives a few examples per edge type plus the true total — for § 823 BGB that total is 3,460. This endpoint returns the list itself, paged, so you can actually work through it. direction=in (default) is what cites this, out what it cites; the response always includes edge_types with counts so you know what you can filter on.

Ordered by authority (PageRank) — leading decisions first — with a stable tie-break, so paging never repeats or skips an entry. That matters here: most edges share a score of 0, and without a total order the same decision would surface on two pages while another never appeared.

POST /api/v1/verify{ "citations": [ … ≤200 ] } or { "text": "…" }

The hallucination check. Verify every citation in a drafted answer in one call: per-citation verified/unresolved status, canonical URI, source and pinpoint unit.

A pinpoint is part of the claim: § 573 Abs. 6 BGB comes back unresolved, because that provision ends at paragraph 4 — with norm_exists: true, unit_exists: false and a reason, so a client can tell "invented paragraph in a real provision" from "invented provision". The same holds on /resolve: exists answers for the cited pinpoint, not merely for the provision it sits in, so a gate that checks one flag is never told an invented paragraph is fine. The provision itself still comes back under object, flagged norm_exists. Where the paragraph list cannot be established with certainty, the citation stays verified and carries unit_requested plus a hint instead — kvasir does not claim a gap it cannot prove.

POST /api/v1/check-draft{ "text": "…" }

The grounding gate. One call over a full draft, built to run before an AI answer ships: verifies every citation, checks verbatim quotes against the canonical source wording (similarity + source excerpt on deviation), and flags repealed law and pending legislative changes to cited acts. Returns summary.verdict (pass | warn | fail) plus per-finding severities.

GET /api/v1/object/<uri>?include=…

One object. include is comma-separated; see below.

POST /api/v1/objects{ "uris": [ … ≤100 ], "include": "content" }

Batch-resolve many URIs in one call — e.g. to ground every citation in a draft at once.

GET /api/v1/search?q=…&kinds=norm,decision&quellen=bund,by,eu&limit=20&offset=0

Free-text search → ranked hits [{uri, kind, title, snippet, score}]. Filter by type (kinds: norm, decision, or opt-in material for legislative materials — EU proposals and Commission guidelines, German federal bills and BaFin acts, distinguished by typ) and jurisdiction (quellen) so every result slot counts. Paginates via offset + has_more; per-source caps bound the result pool at roughly 50 hits — refine the query rather than paging deep. A degraded field, when present, lists sub-searches that failed: treat those results as incomplete, not as absence.

GET /api/v1/changes?since=2026-07-01T00:00:00Z&kinds=norm,decision

Delta-sync. Every object changed since since, as URIs with timestamps. Continue with the response's next_cursor (keyset — safe across bulk reindexes; plain next_since also provided). Rows may repeat at page boundaries — keep client upserts idempotent.

GET /api/v1/status — live corpus counts + freshness per jurisdiction (public, cached).

Per source: *_updated (last write) vs. *_checked (last successful check against the official source, including runs that confirmed nothing changed), check_cadence and freshness per scope. Beyond norms and decisions, a checks object carries the remaining scopes — notably repeal: when it was last verified that acts of this jurisdiction are still in force. For a currency gate, that is the scope to watch; a fresh text says nothing about continued validity.

GET /api/v1/schema — the contract, machine-readable (public).

GET /api/v1/openapi.json — full OpenAPI 3.1 spec (public). Import into Postman or generate a client SDK.

The quellen parameter (jurisdiction filter)

Every search accepts a jurisdiction filter — comma-separated, on /api/v1/search as quellen= (alias jurisdiction=) and on the MCP tool search_legal as quellen. Default is all nine. Filter server-side whenever the question is jurisdiction-specific: every result slot is then spent where the answer actually lives, instead of being crowded out by federal German hits. An unknown code returns 400 with the valid list rather than silently ignoring the filter.

CodeJurisdictionContains
bundGermany — federalStatutes & regulations, federal court decisions (BVerfG, BGH, BVerwG, BAG, BSG, BFH), legislative materials (Bundestag bills, BaFin acts)
byBavariaState law, Bavarian court decisions
nrwNorth Rhine-WestphaliaState law (SGV), NRWE case law
snSaxonyState law (REVOSAX)
bbBrandenburgState law (BRAVORS)
hbBremenState law
atAustriaFederal law (RIS), OGH / VfGH / VwGH case law
chSwitzerlandFederal law (Fedlex, SR)
euEuropean UnionRegulations & directives, CJEU case law, legislative materials

GET /api/v1/search?q=Datenschutz&jurisdiction=at,ch&limit=5 → Austrian and Swiss hits only. The response echoes the active filter in its quellen field, so a client can tell a narrowed search from a full one.

The include parameter

ValueAdds
contentFull text + html + metadata (default).
noneIdentity, provenance and (on /resolve) the pinpoint unit — without the full text. The lightweight "does it exist, and where exactly?" call.
unitsAddressable sub-units with exact text, citation & deeplink — norm = paragraph/number/letter down to the individual sentence and half-sentence (a1.ba, a2.s1, a2.n3.h2), decision = Randnummer (r17). Fidelity-validated: only sub-units whose text verifies against the full text are returned; units_confidence reports the pass rate.
versionsA norm's earlier wordings as normversion: URIs (where tracked).
childrenOn a statute (gesetz:): the full table of contents — every norm as [{uri, label, section_path}] in order. Ask "which sections does the BDSG have?" in one call.
relationsCitation graph: what cites it / what it cites, instances, concepts — and on statutes the legislative layer: for EU acts the founding Commission proposal (begruendet), pending amendment proposals (aenderung_geplant), delegated/implementing acts (basiert_auf) and Commission guidelines (konkretisiert); for German federal law pending Bundestag bills with the exact provisions they would amend (aendert) and BaFin acts (konkretisiert). Ask "what changes are planned for the GDPR?" — or for the BGB — in one call.
authorityPageRank-style authority score within the graph.
commentaryAnchored, author-attributed legal commentary on the object (never merged; each entry names its author and anchor level). &commentary_scope=inherit adds the enclosing levels (section preliminary remarks, statute introduction).
allEverything above.

Use it as an MCP tool

Each tool below is documented in full — parameters, returns, required scope and how it handles the data you send — in the MCP tool reference.

Any MCP client calls the layer natively — endpoint https://kvasir.legal/mcp (OAuth or X-API-Key). Seven tools:

ToolWhat it does
resolve_citationCitation string → verifiable object + pinpoint unit. The preferred entry point.
verify_citationsBatch hallucination check (≤200) for a bare list of citations.
check_draftThe verification layer for a full draft: citations + quoted wording + repealed/pending-change flags → pass | warn | fail.
search_legalSemantic + full-text search → ranked canonical URIs.
get_legal_relationsPage the citation graph from one URI — every decision citing a provision, ordered by authority.
get_legal_objectOne URI → object with provenance, units, versions, relations.
get_legal_objectsBatch resolve (≤100 URIs).
legal_uri_grammarURI scheme reference for constructing URIs.

Rate limits & errors

Errors are JSON: { "error", "hint", "docs_url" } (429s also carry a code: rate_limited | quota_exceeded | sandbox_limit). 401 = missing/invalid key · 429 = limit reached · 503 = brief infrastructure hiccup (your key is fine — retry) · exists:false means the URI matched nothing (not a server error).

Defaults: 120 requests/min and 50,000/month per key (design partners: tell us what you need). Every response carries X-RateLimit-Limit / -Remaining / -Reset; 429s add Retry-After. Sandbox responses carry X-Sandbox-Remaining.