kvasir.legal

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

文档

REST API

A read-only HTTP API over German, Austrian, Swiss and EU law. Resolve a citation to the provision it names, check wording against the source, and walk the citation graph. Every object carries its official source, its version, and a pinpoint down to the sentence.

Base URLhttps://kvasir.legal/api/v1
AuthAuthorization: Bearer <key> — read endpoints also answer without one, capped per IP
FormatJSON. No SDK required.
Also as MCPhttps://kvasir.legal/mcp — tool reference
Specopenapi.json · explorer
curl https://kvasir.legal/api/v1/resolve?cite=%C2%A7+573+Abs.+2+Nr.+2+BGB

Questions: [email protected] · background: why legal AI needs a grounding layer · measured: the Grounding Benchmark

Keys. Create an account, confirm the email address, then issue a key in the dashboard. Without a key the read endpoints answer 20 requests per day and IP.

Higher volume, more coverage or an endpoint that does not exist yet — write to us:

Connecting an MCP client

One endpoint, https://kvasir.legal/mcp, streamable HTTP. Authenticate with OAuth or an X-API-Key header. Setup per client below; the tools themselves are documented in the MCP reference.

https://kvasir.legal/mcp/demo — the same eight tools without a key: 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 eight 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 plain REST.

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

Without a key, five read endpoints answer in sandbox mode — /resolve, /search, /object, /relations and /verify — capped at 20 requests per day and IP. The response carries X-Sandbox: true and X-Sandbox-Remaining:

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 to any other endpoint, 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

quelle ∈ bund (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

Every endpoint is also clickable in the explorer. 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".

German wording for EU acts. The EU corpus is indexed in English — one language, one canon, one embedding space. For a German quotation check that is unusable: Art. 6 DSGVO returns "processing shall be lawful only if …" while the document under review says "Die Verarbeitung ist nur rechtmäßig, wenn …", and every language difference reads as a citation error. Add &lang=de to /api/v1/resolve or /api/v1/object and you get the German expression, fetched live from EUR-Lex; all language versions of an EU act are equally authentic, so this is not a translation but the other authentic wording. content.language always states which one you received — if the German version cannot be retrieved, the answer says so instead of passing English text off as German. check_draft does this by itself: a German quotation that fails against the English text is re-checked against the German one before any finding is raised.

Artikel and Paragraphen. Einführungsgesetze are divided into Artikel and only below that into Paragraphen, so in Art. 246a § 1 EGBGB the § 1 is part of the address, not a pinpoint inside the article. It resolves to that exact provision; a § the article does not have comes back unresolved with its siblings as suggestions, rather than as the article's first paragraph. Citing the wrong divider still resolves — § 1 EGBGB finds Art. 1 — but the answer carries a marker_note saying the statute uses Artikel, because silently equating the two makes a citation error look like a confirmation.

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 runs into the thousands. 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 ], "quellen": "by" } 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": "…", "quellen": "by" }

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 endpoint that has to choose a provision accepts a jurisdiction filter — comma-separated, on /api/v1/search as quellen= (alias jurisdiction=), on /api/v1/resolve as a query parameter, and on /api/v1/verify and /api/v1/check-draft as a quellen field in the JSON body. The MCP tools search_legal, resolve_citation, verify_citations and check_draft take the same argument. 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.

On verification the filter decides what "verified" means. Art. 141 BV is the Swiss constitution and the Bavarian one, and both have an Art. 141. Without a filter kvasir resolves by jurisdiction priority and says so — an ambiguous block on /verify, an ambiguous_jurisdiction warning and a warn verdict on /check-draft. Pass "quellen": "by" and the citation is checked against Bavarian law, with no warning left over. If you know the jurisdiction your text is written for, set it: it turns a flagged guess into an answer.

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

120 requests per minute and key. Monthly volume is counted in sips, not requests: a sip is a unit of work, so check_draft weighs five times a resolve, and a batch costs per item. The free plan includes 2,000 sips per month; current usage is in your dashboard.

Keyed responses carry X-RateLimit-Limit / -Remaining / -Reset, keyless ones X-Sandbox / X-Sandbox-Remaining. 429 s add Retry-After.