Lingara Lingara Dokumentation Ratgeber API Bibliotheken Apps Erstellen Web-App
Sprache: Deutsch

Webhooks und Ereignisse

API-Version 2026-10-affable-towhee

Lingara erfasst als Ereignisse, was mit den Lektionsplänen und der Nutzung deines Kontos geschieht, und nimmt Ereignisse von deinem Spiel oder deiner App entgegen. Jedes Ereignis hat, egal auf welchem Weg es kommt, dieselbe Hülle, und der Ereigniskatalog listet sie alle auf.

Die Hülle

Jedes Ereignis trägt sechs Felder. id beginnt mit lgr_evt_, ist eindeutig und ist der Schlüssel zur Deduplizierung. type benennt das Ereignis. created_at gibt an, wann es geschah. api_version ist die Version, in deren Form data vorliegt: die Version, an die dein Client gebunden ist, oder, beim Feed und beim Stream, die Version, die deine Anfrage in Lingara-Version genannt hat. subject beginnt mit lgr_sub_ und sagt, wen das Ereignis betrifft: Es ist für deinen Client stabil, aber für jeden Client anders, und es ist nie eine E-Mail-Adresse, ein Name oder eine Konto-ID. data ist klein und benennt Ressourcen, statt sie zu kopieren: Rufe eine Ressource mit dem Scope ab, den sie benötigt.

Zwei Typen brauchen je einen Satz. lesson_plan.ready kann für einen Plan zweimal eintreffen, zuerst mit data.status partial und dann complete: Reagiere auf das erste für einen nutzbaren Plan, oder warte auf complete, um alle Sets zu haben. usage.threshold_reached wird nur für nutzungsbasiert abgerechnete Konten und Clients gesendet, und ein Sprung über mehrere Schwellen meldet nur die höchste überschrittene: Erwarte also nicht ein Ereignis pro Schwelle.

Ein Protokoll, drei Wege, es zu hören

Webhooks eignen sich für einen Server mit einem öffentlichen HTTPS-Endpunkt. Feed und Stream eignen sich für ein Programm ohne einen solchen, etwa ein Spiel auf dem Rechner eines Spielers. Die Hülle ist auf jedem Weg dieselbe, sodass ein Programm mit dem Feed beginnen und später zu Webhooks wechseln kann, ohne zu ändern, wie es ein Ereignis liest. Die Ereignisrouten bedienen native Programme. Ein Spiel, das im Browser läuft, kann sie noch nicht aufrufen, weil /v1/ auf keinen Cross-Origin-Preflight antwortet.

Wer ein Ereignis hört

Ein Client hört ein Ereignis, wenn er events:read und den eigenen Scope des Ereignistyps besitzt, den der Katalog nennt, und wenn das Ereignis den Eigentümer des Clients betrifft. Beim Feed und beim Stream schränken die Scopes des Zugriffstokens dies weiter ein, und types schränkt es auf die Typen ein, die du nennst. webhook.test geht nur an den Endpunkt, an den es gesendet wurde, nie an den Feed, und kann nicht abonniert werden. app.installed und app.uninstalled gehen nur an den eigenen Client der App, nie an einen anderen Client desselben Kontos.

Einen Endpunkt registrieren

Registriere einen Endpunkt auf der Seite Webhooks der Lingara-Web-App unter app.getlingara.com/admin/webhooks und wähle dabei zuerst den Client. Seine URL muss https auf Port 443 verwenden, und sein Host darf sich nur in öffentliche Adressen auflösen. Wähle unter „Zu sendende Ereignisse“ die Ereignisse aus: Angeboten werden nur die Typen, die die Scopes des Clients erlauben. URL und Ereignisse lassen sich später nicht bearbeiten: Füge einen neuen Endpunkt hinzu und lösche den alten. Das Signatur-Secret beginnt mit lgr_whsec_ und wird nur einmal angezeigt.

Eine Zustellung prüfen

Jede Zustellung ist ein POST mit drei Headern gemäß der Spezifikation Standard Webhooks: webhook-id (die id des Ereignisses), webhook-timestamp und webhook-signature. Der HMAC-Schlüssel ist der base64-dekodierte Teil des Secrets nach lgr_whsec_, nie das Secret als Zeichenkette. Prüfe vor dem Parsen des Bodys, über seine rohen Bytes, wie unten. Lehne eine Zustellung ab, deren Zeitstempel mehr als fünf Minuten von jetzt abweicht, der Standardwert der Standard-Webhooks-Bibliotheken: Das verhindert, dass eine abgefangene Zustellung erneut abgespielt wird.

signed   = webhook-id + "." + webhook-timestamp + "." + raw request body
key      = base64_decode(the secret after its prefix)
expected = "v1," + base64(hmac_sha256(key, signed))
accept   if |now - webhook-timestamp| <= 5 minutes
         and some entry of webhook-signature (space-separated) equals expected
             (compare in constant time)

Standard Webhooks veröffentlicht Prüfbibliotheken für die meisten Sprachen. Sie erwarten ein Secret in der Form whsec_ gefolgt von base64 oder als reines base64, also übergib ihnen den Teil des Lingara-Secrets nach lgr_whsec_. Die eigenen Bibliotheken von Lingara akzeptieren das ganze Secret.

Schnell antworten, mit Wiederholungen rechnen

Antworte innerhalb von 10 Sekunden mit einem beliebigen 2xx und erledige die Arbeit danach. Alles andere, auch eine Zeitüberschreitung oder ein 3xx (Weiterleitungen werden nicht verfolgt), wird mit wachsenden Abständen etwa einen Tag lang wiederholt. Ein 410 als Antwort auf eine automatische Zustellung deaktiviert den Endpunkt sofort; ein 410 als Antwort auf einen Test oder eine erneute Zustellung nicht. Nach fünf Tagen fehlgeschlagener Zustellungen wird der Endpunkt ebenfalls deaktiviert. In beiden Fällen erhält sein Eigentümer eine E-Mail. Ein Fehler auf Lingaras Seite zählt nie zur Deaktivierung eines Endpunkts. Auf der Seite Webhooks kannst du mit „Test senden“ einen Test schicken oder jede Zustellung der letzten 30 Tage mit „Erneut zustellen“ noch einmal senden. Beides ist jeweils ein einziger Versuch, wird nie wiederholt und auch an einen deaktivierten Endpunkt gesendet.

Die Zustellung erfolgt mindestens einmal und ungeordnet. Dasselbe Ereignis kann zweimal eintreffen, und eine Wiederholung kann nach einem späteren Ereignis eintreffen. webhook-id ist bei jeder Wiederholung gleich, auch bei einer erneuten Zustellung bis zu 30 Tage später. Speichere jede verarbeitete id 30 Tage lang und ignoriere eine Wiederholung. Sortiere nach created_at, wenn die Reihenfolge wichtig ist.

Ein Signatur-Secret rotieren

Ein Endpunkt kann zwei Signatur-Secrets gleichzeitig besitzen. Solange beide aktiv sind, trägt webhook-signature zwei v1,-Einträge, und ein Empfänger, der eines von beiden akzeptiert, funktioniert weiter. Füge das neue Secret deinem Server hinzu, stelle es bereit und widerrufe dann das alte.

Der Feed

GET /v1/events mit dem Zugriffstoken eines Clients liefert items (Hüllen), next_cursor und has_more. Das Token benötigt events:read und den Scope jedes Typs, den du hören willst: Mit events:read allein ist der Feed leer. Er beginnt ab jetzt. Übergib start=oldest für die Ereignisse der letzten etwa 30 Tage. Er braucht weder einen öffentlichen Endpunkt noch ein Signatur-Secret: Das Zugriffstoken beweist, wer fragt. Der Austausch unten fordert beide Scopes an, die die Lektionsplan-Ereignisse benötigen.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=events:read lesson_plans:read" | jq -r '.access_token // error(.error)')"
curl "https://api.getlingara.com/v1/events" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

next_cursor ist immer vorhanden: Speichere ihn und gib ihn als cursor zurück. Er ist opak. has_more true heißt, sofort erneut aufzurufen, und false heißt, du bist auf dem neuesten Stand: Frage später erneut ab oder öffne den Stream. Ein Cursor, der älter als 30 Tage ist, wird mit 410 und cursor_expired abgelehnt. Ohne Cursor beginnt der Feed ab jetzt, und die Ereignisse dazwischen werden übersprungen. Um sie nachzuholen, rufe mit start=oldest auf, was so weit zurückreicht, wie Ereignisse aufbewahrt werden, und überspringe die id-Werte, die du bereits verarbeitet hast.

Der Stream

GET /v1/events/stream überträgt dieselben Ereignisse als Server-Sent Events. Das data jedes event-Frames ist eine Hülle, und die id: jedes Frames ist ein Cursor, dasselbe Token wie next_cursor, sodass du ohne Lücke zwischen Feed und Stream wechseln kannst. Nach einer abgebrochenen Verbindung, einem done-Frame (der Stream beendet sich von Zeit zu Zeit selbst) oder einem error-Frame verbinde dich erneut mit Last-Event-ID, gesetzt auf die letzte empfangene id:. Die meisten SSE-Clients erledigen das für dich, ebenso tailEvents in den Bibliotheken von Lingara (streamEvents ist dort eine einzelne Verbindung). Es ist ein Cursor, nicht die id des Ereignisses. Der Stream sendet einen Heartbeat, eine stille Verbindung ist also eine tote.

curl -N "https://api.getlingara.com/v1/events/stream" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

Ein Ereignis an Lingara senden

POST /v1/events mit events:write sendet ein Ereignis als {type, data} an Lingara: world.context_changed (eine scene, source_lang, target_lang, level und optional ein npc mit name und persona sowie tags) oder world.practice_requested (ein topic und dieselben Sprachen und dasselbe Niveau). Idempotency-Key ist Pflicht: bis zu 255 sichtbare ASCII-Zeichen, etwa eine UUID. Ohne ihn lautet die Antwort 400 und idempotency_key_required. Setze ihn einmal pro Ereignis und sende bei einer Wiederholung denselben Schlüssel. Ein Schlüssel ist ein Ereignis: Innerhalb eines Tages erhält eine zweite Anfrage mit demselben Schlüssel die erste Antwort (gleich als JSON, nicht Byte für Byte), auch wenn ihr Body abweicht, und danach erhält sie dasselbe Ereignis, wie unten beschrieben. Eingehende Ereignisse werden nicht signiert: Dein Zugriffstoken ist der Nachweis. Beschreibe die Welt, nie den Spieler: keine Namen oder Chats in scene, npc, topic oder tags.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=events:write lesson_plans:write" | jq -r '.access_token // error(.error)')"
curl -X POST "https://api.getlingara.com/v1/events" \
  -H "Authorization: Bearer $LINGARA_TOKEN" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"world.context_changed","data":{"scene":"A night market in Taipei, just after rain","npc":{"name":"Auntie Lin","persona":"a street-food vendor who likes to haggle"},"source_lang":"en","target_lang":"zh","level":3,"tags":["market","food","chapter-2"],"generate":true}}'

Jedes Textfeld ist eine Zeile sichtbarer Zeichen, gezählt nach dem Entfernen von Leerraum am Anfang und Ende: scene und topic bis 160, npc.name bis 32 und npc.persona bis 120. Zeilenumbrüche, Tabulatoren und andere Steuerzeichen werden abgelehnt, ebenso unsichtbare Zeichen und Formatierungszeichen: Richtungsüberschreibungen, Nullbreitenzeichen außer den Verbindern, die manche Schriften und Emoji brauchen, der Tag-Block und Zeichen für den privaten Gebrauch. tags enthält bis zu 8 maschinenlesbare Tokens in Kleinbuchstaben mit je bis zu 24 Zeichen und erreicht nie den Lektionsplan. level liegt zwischen 1 und 9, und die beiden Sprachen müssen sich unterscheiden. Eine Anfrage außerhalb dieser Grenzen wird mit 400 abgelehnt und erfasst kein Ereignis.

Mit "generate": true (dem Standard für world.practice_requested) benötigt das Token außerdem lesson_plans:write. Ohne diesen Scope wird die Anfrage mit 403 abgelehnt und kein Ereignis erfasst. Mit ihm startet Lingara einen Lektionsplan, mit denselben Prüfungen und derselben Abrechnung wie bei direkter Erstellung, und reaction in der 202-Antwort sagt, was geschehen ist. Bei started und plan_status generating folgt ein lesson_plan.ready oder lesson_plan.failed, dessen data.plan_id die plan_id der Antwort ist, auf jedem Weg, den du nutzt. Bei partial oder complete kam der Plan aus der Bibliothek und kann sofort gelesen werden, und es wird kein Ereignis zugesagt: Eines kann trotzdem eintreffen, daher lohnt sich das Warten nur bei generating. Bei refused oder failed bleibt das Ereignis bestehen. Es wird unter demselben Schlüssel nicht wiederholt: Sende für einen neuen Versuch ein neues Ereignis.

Eine Wiederholung innerhalb eines Tages erhält die erste Antwort zurück. Eine spätere Wiederholung wird aus dem gespeicherten Ereignis neu aufgebaut, das den gestarteten Plan behält, nicht aber den Grund, warum eine Reaktion abgelehnt wurde. Eine späte Wiederholung kann daher mit reaction failed und internal antworten: Das bedeutet, dass das erste Ergebnis nicht erfasst wurde, nicht, dass kein Plan existiert. Lies den Plan über seine plan_id, falls du sie aufbewahrt hast, oder sende ein neues Ereignis.

Tidewater Games: ein Spiel ohne Server

Tidewater Games, ein fiktives Studio, entwickelt ein Godot-Spiel, in dem der Spieler einen Nachtmarkt erkundet. Der Entwickler lässt das Spiel auf seinem eigenen Rechner laufen, mit seinem eigenen Client.

Der Spieler betritt einen Nudelstand. Das Spiel sendet world.context_changed mit "generate": true, den Befehl aus dem Abschnitt zu eingehenden Ereignissen oben, und behält die plan_id aus der Antwort.

Ist plan_status gleich generating, liest das Spiel den Stream oder fragt den Feed ab, bis ein lesson_plan.ready mit dieser plan_id eintrifft. Dann liest es den Plan mit lesson_plans:read, wie unten. War der Plan bereits complete, liest es ihn sofort.

export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
  -u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  --data-urlencode "scope=lesson_plans:read" | jq -r '.access_token // error(.error)')"
curl "https://api.getlingara.com/v1/lesson-plans/$ID" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

Später ergänzt das Studio einen kleinen Server mit einem HTTPS-Endpunkt und registriert ihn für lesson_plan.ready. Dasselbe Ereignis kommt dort an, mit derselben id, und der Code des Spiels, der es liest, ändert sich nicht.

Ein Client-Secret darf nie in einem Spiel-Build ausgeliefert werden, denn alles auf dem Gerät eines Spielers kann gelesen werden. Bis Lingara die Anmeldung im Namen eines Spielers unterstützt, spricht ein Spiel auf den Rechnern der Spieler mit seinem eigenen Server, und nur die eigene Kopie des Entwicklers spricht direkt mit Lingara.

Was Ereignisse kosten

Die Nutzung deines Clients zählt jedes angenommene eingehende Ereignis, jeden Feed-Aufruf und jeden geöffneten Stream, so wie sie jeden /v1/-Aufruf zählt. Ein mit "generate": true gesendetes Ereignis zählt zusätzlich als Lektionsplan. Jede Webhook-Zustellung zählt einmal pro Ereignis und Endpunkt, bei ihrem ersten 2xx, egal bei welchem Versuch. Sie zählt nie erneut, und ein Test zählt nie. GET /v1/usage zeigt den bisherigen Monat.

Weichen eine Übersetzung und die englische Referenz voneinander ab, ist die englische Referenz maßgeblich.