Zum Inhalt springen
JuNoDaJuNoDa
Schnittstelle

API für Entwickler

Rechercheantworten über den deutschen Rechtsbestand — mit Fundstelle, Rechtsprechung und geprüften Zitaten. Gedacht für Kanzleien und Kanzleisoftware, die einen eigenen Chat anbieten wollen, dessen Verarbeitung durch unsere Infrastruktur läuft.


Was die API liefert — und was nicht

Sie liefert von uns erarbeitete Ergebnisse: Antworten mit Fundstellen, Verweise und Passivverweise, typisierte Zitierungen zwischen Entscheidungen, Graph-Nachbarschaften.

Dazu eine Leistung, die in die andere Richtung läuft: POST /zitate liest Ihren Text und sagt Ihnen, was darin zitiert wird.

Sie liefert keinen Korpus. Es gibt keinen Endpunkt, der eine Norm oder eine Entscheidung im Volltext herausgibt; Fundstellen werden über ihre zitierfähige Kennung benannt und verlinkt. In Antworten wird zitiert, aber eine zusammenhängende Wortlautpassage über 1200 Zeichen wird gekürzt — erkennbar am Feld wortlautGekuerzt.

Zustandslos

Wir speichern keine Gespräche. Wer eine Unterhaltung fortsetzen will, schickt den verlauf bei jeder Anfrage erneut mit. Gespeichert werden ausschließlich Zähler — Tokens und Kosten, keine Inhalte.

Das ist Absicht und der Kern der Datenschutzzusage: Es gibt bei uns nichts, was gelöscht werden müsste.

Authentifizierung

Authorization: Bearer junoda_…. Jeder Schlüssel gehört einer Kanzlei; eine Kanzlei kann beliebig viele mit unterschiedlichen Scopes führen. Ein Endpunkt verlangt genau einen Scope.

Verfügbare Scopes: recherche:antwort, recherche:fundstellen, graph:lesen. Der Schlüssel wird bei uns nur als Hash gespeichert — er lässt sich nach dem Anlegen nicht erneut anzeigen, nur ersetzen.

Endpunkte

MethodePfadScopeZweck
POST/antwortenrecherche:antwort

Juristische Frage beantworten — als JSON oder als SSE-Strom.

POST/zitaterecherche:fundstellen

Zitate in einem übergebenen Text erkennen — Kennung und Position.

GET/schluesselgraph:lesen

Auskunft über den eigenen Schlüssel. Kostet keine Recherche.

GET/normen/{abk}/{ordnungszahl}/verweisegraph:lesen

Verweise einer Norm, aktiv und passiv.

GET/normen/{abk}/{ordnungszahl}/zitiert-vongraph:lesen

Alias auf obigen Endpunkt, nur die Passivseite.

GET/entscheidungen/{kennung}/verweisegraph:lesen

Verweise einer Entscheidung, aktiv und passiv.

GET/entscheidungen/{kennung}/zitierungengraph:lesen

Alias auf obigen Endpunkt, nur der Zitiergraph zu anderen Entscheidungen.

GET/verwaltungsvorschriften/{kennung}/verweisegraph:lesen

Verweise einer Verwaltungsvorschrift, aktiv und passiv.

GET/knoten/{typ}/{kennung}/verweisegraph:lesen

Dieselben Verweise, typunabhängig über ein Pfadsegment adressiert.

Basis-URL https://junoda.de/api/v1. Die vollständige, maschinenlesbare Beschreibung aller Felder steht in der OpenAPI-3.1-Datei — sie ist ohne Schlüssel abrufbar und lässt sich direkt in Swagger UI, Scalar oder einen Codegenerator laden.

Jede Kante trägt ihre Gegenstelle mit einer gegenstelle.kennung — die passt unverändert, URL-kodiert, in /knoten/{typ}/{kennung}/verweise: wer einer Kante folgen will, muss dafür kein eigenes Mapping von Knotentyp auf Pfadform pflegen.

Eine Anfrage

bash
curl -N https://junoda.de/api/v1/antworten \
  -H "Authorization: Bearer $JUNODA_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "frage": "Wann ist die Miete wegen Schimmel gemindert?",
    "verlauf": []
  }'

Ohne Accept: text/event-stream kommt dieselbe Antwort gepuffert als JSON zurück. Ein Endpunkt, zwei Darreichungen.

Der Strom

text/event-stream
event: meta
data: {"requestId":"…","modell":"…","verarbeitung":"JUNODA_SELBST_GEHOSTET"}

event: delta
data: {"text":"Nach § 536 BGB "}

event: result
data: {"inhalt":"Nach [§ 536 BGB](https://junoda.de/de/gesetze/bgb/536) …",
data: "quellen":[{"zitierkennung":"§ 536 BGB","benutzt":true,…}],
data: "unbelegteZitate":[],"keinTreffer":false,…}

event: done
data: {}

Zwei Dinge, die man hier leicht falsch macht:

  1. Die delta-Texte sind nicht das Ergebnis. Sie sind fürs Tippbild da. Wer sie nur aneinanderhängt, bekommt einen Text ohne Zitat-Links und ohne die Kürzung überlanger Wortlautpassagen — ersetzen Sie ihn, sobald result kommt, durch dessen inhalt.

  2. Quellen kommen erst am Ende. Die Prüfung, ob das Modell etwas zitiert hat, das in keiner mitgelieferten Vorschrift steht, braucht den vollständigen Text. Sie lässt sich nicht mitstreamen.

Kommentarzeilen (: ping) alle 15 Sekunden halten die Verbindung offen; SSE-Clients ignorieren sie. Schließen Sie die Verbindung, bricht der Modellaufruf ab und kostet nicht weiter.

Zitate in Ihrem Text erkennen

POST /zitate läuft in die andere Richtung als alles übrige: Sie schicken einen Text — Schriftsatz, Gutachten, Mail, Bescheid —, und wir geben zurück, was darin zitiert wird. Kennung und Position, sonst nichts. Kein Titel, kein Link, kein Wortlaut. Es ist dieselbe Erkennung, mit der Junoda in der eigenen Oberfläche Zitate im Fließtext markiert, nur als Daten statt als Link.

bash
curl https://junoda.de/api/v1/zitate \
  -H "Authorization: Bearer $JUNODA_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Der Anspruch folgt aus § 536 Abs. 1 BGB; vgl. BGH, Urteil vom 6. April 2017 - 3 StR 5/17." }'

{
  "normen": [
    { "positionVon": 22, "positionBis": 38, "text": "§ 536 Abs. 1 BGB",
      "einheit": "PARAGRAF", "abk": "BGB", "ordnungszahl": "536", "absatz": "1", … }
  ],
  "entscheidungen": [
    { "positionVon": 45, "positionBis": 90, "text": "BGH, Urteil vom 6. April 2017 - 3 StR 5/17",
      "gericht": "BGH", "datum": "2017-04-06", "aktenzeichen": "3 StR 5/17", … }
  ],
  "verwaltungsvorschriften": [],
  "tarifvertraege": []
}

Vier Dinge, die das Schema nicht sagt:

  1. Die Positionen zählen UTF-16-Codeeinheiten. Das ist die Zähleinheit von JavaScript — Go zählt Bytes, Python zählt Codepoints, und der Versatz beginnt beim ersten Umlaut vor der Fundstelle. Deshalb trägt jeder Treffer seinen Ausschnitt als text mit: prüfen Sie einmal dagegen, bevor Sie damit markieren. Und schicken Sie den Text unverändert — trimmen oder kürzen verschiebt alles.

  2. Ein Treffer sagt nichts über unseren Bestand. Erkannt wird auch ein Zitat auf eine Vorschrift, die wir gar nicht führen — die Antwort ist eine Aussage über Ihren Text, keine über unseren Korpus. Zu einem Treffer kann /normen/…/verweise deshalb 404 liefern, und abk ist die Abkürzung aus Ihrem Text, keine von uns geprüfte Kennung.

  3. Kein Sprachmodell im Spiel. Reine Mustererkennung: dieselbe Eingabe ergibt immer dieselbe Ausgabe, in Millisekunden. Ihr Text wird nicht gespeichert und an keinen Dritten gegeben — bei einem Schriftsatz aus einer Mandantenakte ist das der Unterschied, auf den es ankommt.

  4. Tarifverträge stehen getrennt. TVöD, BAT und MTV sehen im Text aus wie Gesetze und kommen deshalb in einer eigenen Liste, nicht unter normen. Ein Bereich bleibt dabei eine Fundstelle: „§§ 535 bis 537 BGB" ist ein Treffer mit istBereich: true, nicht drei.

unbelegte Zitate

Jede Antwort trägt ein Feld unbelegteZitate: Zitate aus der Modellantwort, die in keiner der mitgelieferten Vorschriften stehen. Ist die Liste nicht leer, gehört das dem Nutzer angezeigt — das ist die Halluzinationsprüfung, und sie ist der Grund, warum diese Antworten in einem Rechtsprodukt vertretbar sind.

Ebenso ist keinTreffer: true etwas anderes als eine leere Antwort: es heißt, dass zum Thema nichts im Bestand lag. Die Verwechslung ist die gefährliche Richtung.

Fehler

Immer application/problem+json nach RFC 9457. Stabil ist der type-URI — darauf sollte sich Code stützen, nicht auf status oder title.

http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 27
Retry-After: 27

{
  "type": "https://junoda.de/fehler/ratenlimit",
  "title": "Zu viele Anfragen",
  "status": 429,
  "requestId": "0199c4e1-…",
  "retryAfter": 27
}

Beim Streamen gilt eine Besonderheit: sobald der Strom läuft, ist der HTTP-Status (200) längst gesendet. Ein Fehler danach kommt als error-Ereignis mit demselben Dokument im Rumpf — Ihr Auswertungspfad bleibt derselbe.

Kontingente

Jede Antwort trägt RateLimit-Limit, RateLimit-Remaining und RateLimit-Reset (Sekunden). Gemeldet wird stets das knappere der beiden Fenster — Minute oder Kalendermonat —, damit Sie die Grenze sehen, in die Sie als Nächstes laufen. Bei 429 zusätzlich Retry-After.

Abgelehnte Anfragen zählen mit. Sammelantworten sind auf 100 Einträge je Seite gedeckelt, und jede Seite zählt als eigene Anfrage.

Datenschutzmodus

Mit "datenschutzModus": true läuft die Anfrage über unser selbst gehostetes Modell statt über den Standardanbieter. Jede Antwort weist unter meta.verarbeitung aus, was tatsächlich verwendet wurde — damit können Sie das gegenüber Ihren eigenen Mandanten belegen.

Ein Schlüssel kann den Modus fest erzwungen tragen. Dann gilt er für jede Anfrage und lässt sich per Feld nicht zurückschalten: Das Feld kann nur verschärfen, nie lockern — damit eine Zusage an Ihre Mandanten nicht an einem vergessenen Flag in Ihrem Code scheitert.


Einen Schlüssel erhalten Sie über Ihre Kanzlei-Verwaltung. Für die Verarbeitung personenbezogener Daten Ihrer Mandanten schließen wir einen Auftragsverarbeitungsvertrag nach Art. 28 DSGVO — sprechen Sie uns an.