Webhook とイベント
API バージョン 2026-10-affable-towhee
Lingara は、アカウントのレッスンプランと使用状況に起きたことをイベントとして記録し、あなたのゲームやアプリからのイベントも受け付けます。どの経路で届くイベントにも同じエンベロープがあり、イベントカタログにすべてが一覧されています。
エンベロープ
すべてのイベントは6つのフィールドを持ちます。id は lgr_evt_ で始まる一意の値で、重複排除のキーになります。type はイベントの種類を表します。created_at はイベントが起きた日時です。api_version は data の形の基準となるバージョンで、クライアントの固定バージョン、またはフィードとストリームではリクエストが Lingara-Version で指定したバージョンです。subject は lgr_sub_ で始まり、そのイベントが誰に関するものかを表します。同じクライアントに対しては変わりませんが、クライアントごとに異なり、メールアドレス、名前、アカウント ID になることはありません。data は小さく、リソースをコピーせずに名前で示します。リソースは、それに必要なスコープで取得してください。
2つの種類には補足が必要です。lesson_plan.ready は1つのプランについて2回届くことがあります。最初は data.status が partial、次に complete です。すぐ使えるプランが欲しければ最初のものに応じ、すべてのセットが必要なら complete を待ってください。usage.threshold_reached は従量課金のアカウントとクライアントにだけ送られます。一度に複数のしきい値を超えた場合は、超えたうち最も高いものだけが報告されるため、しきい値ごとに1つのイベントが届くとは考えないでください。
1つのログ、3つの受け取り方
Webhook は、公開 HTTPS エンドポイントを持つサーバーに向いています。フィードとストリームは、プレイヤーのマシン上のゲームのように、公開エンドポイントを持たないプログラムに向いています。エンベロープはどの経路でも同じなので、プログラムはフィードから始めて、後でイベントの読み方を変えずに Webhook へ移行できます。イベントのルートはネイティブのプログラムに応答します。ブラウザーで動くゲームはまだ呼び出せません。/v1/ がクロスオリジンのプリフライトに応答しないためです。
イベントを受け取るのは誰か
クライアントがイベントを受け取るのは、events:read とそのイベントの種類自体のスコープ(カタログに記載)を持ち、かつイベントがクライアントの所有者に関するものである場合です。フィードとストリームでは、アクセストークンのスコープによってさらに絞り込まれ、types によって指定した種類だけに絞り込まれます。webhook.test は送信先のエンドポイントにだけ届き、フィードには届かず、購読することもできません。app.installed と app.uninstalled はそのアプリ自身のクライアントにだけ届き、同じアカウントの別のクライアントには届きません。
エンドポイントを登録する
Lingara ウェブアプリの「Webhook」ページ(app.getlingara.com/admin/webhooks)で、まずクライアントを選んでからエンドポイントを登録します。URL はポート 443 で https を使う必要があり、ホストは公開アドレスだけに解決される必要があります。「送信するイベント」を選んでください。クライアントのスコープが許可する種類だけが選択肢に表示されます。URL とイベントは後から編集できません。新しいエンドポイントを追加し、古いものを削除してください。署名シークレットは lgr_whsec_ で始まり、表示されるのは一度だけです。
配信を検証する
すべての配信は、Standard Webhooks 仕様に従った3つのヘッダー付きの POST です。webhook-id(イベントの id)、webhook-timestamp、webhook-signature です。HMAC の鍵は、シークレットの lgr_whsec_ より後の部分を base64 デコードしたもので、シークレットの文字列そのものではありません。本文をパースする前に、以下のように生のバイト列に対して検証してください。タイムスタンプが現在から5分を超えてずれている配信は拒否してください。これは Standard Webhooks ライブラリの既定値で、傍受された配信の再送を防ぎます。
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 は、ほとんどの言語向けに検証ライブラリを公開しています。これらは whsec_ に base64 が続く形式か、base64 だけの形式のシークレットを想定しているため、Lingara のシークレットの lgr_whsec_ より後の部分を渡してください。Lingara 自身のライブラリはシークレット全体を受け付けます。
すばやく応答し、再試行に備える
10 秒以内に任意の 2xx で応答し、処理はその後で行ってください。それ以外の応答は、タイムアウトや 3xx(リダイレクトはたどりません)も含め、間隔を広げながら約1日再試行されます。自動配信への応答が 410 の場合、エンドポイントはただちに無効になります。テストや再配信への 410 では無効になりません。5日間配信が失敗し続けた場合も、エンドポイントは無効になります。どちらの場合も、所有者にメールが送られます。Lingara 側の障害が、エンドポイントの無効化に数えられることはありません。「Webhook」ページからは、テストを送信したり、過去 30 日間の任意の配信を再配信したりできます。どちらも1回だけの試行で再試行されず、無効なエンドポイントにも送信されます。
配信は少なくとも1回行われ、順序は保証されません。同じイベントが2回届くことも、再試行が後のイベントより遅れて届くこともあります。webhook-id はすべての再試行で同じで、30 日以内の再配信でも同じです。処理した各 id を 30 日間記録し、繰り返しは無視してください。順序が重要な場合は created_at で並べてください。
署名シークレットをローテーションする
1つのエンドポイントは同時に2つの署名シークレットを持てます。両方が有効な間は webhook-signature に v1, のエントリーが2つ含まれ、どちらでも受け付ける受信側は動き続けます。新しいシークレットをサーバーに追加してデプロイし、それから古いシークレットを取り消してください。
フィード
クライアントのアクセストークンで GET /v1/events を呼び出すと、items(エンベロープ)、next_cursor、has_more が返ります。トークンには events:read と、受け取りたい各種類のスコープが必要です。events:read だけではフィードは空になります。フィードは現在から始まります。約 30 日前までのイベントが欲しい場合は start=oldest を渡してください。公開エンドポイントも署名シークレットも不要で、誰が要求しているかはアクセストークンが証明します。以下の交換は、レッスンプランのイベントに必要な両方のスコープを要求します。
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 は常に含まれます。保存しておき、cursor として渡し返してください。中身は不透明です。has_more が true ならすぐにもう一度呼び出し、false なら追いついています。後でポーリングするか、ストリームを開いてください。30 日より古いカーソルは 410 と cursor_expired で拒否されます。カーソルなしではフィードは現在から始まり、その間のイベントは飛ばされます。それらを取り戻すには、イベントが保持されている範囲までさかのぼる start=oldest で呼び出し、処理済みの id 値はスキップしてください。
ストリーム
GET /v1/events/stream は、同じイベントを Server-Sent Events として運びます。各 event フレームの data は1つのエンベロープで、各フレームの id: はカーソルです。これは next_cursor と同じトークンなので、フィードとストリームを切れ目なく切り替えられます。接続が切れたとき、または done フレーム(ストリームはときどき自ら終了します)や error フレームを受け取ったときは、最後に受け取った id: を Last-Event-ID に設定して再接続してください。ほとんどの SSE クライアントはこれを自動で行い、Lingara のライブラリの tailEvents も同様です(そこでの streamEvents は単一の接続です)。これはカーソルであり、イベントの id ではありません。ストリームはハートビートを送るので、無音の接続は切れた接続です。
curl -N "https://api.getlingara.com/v1/events/stream" \
-H "Authorization: Bearer $LINGARA_TOKEN"Lingara にイベントを送る
events:write を持つ POST /v1/events は、イベントを {type, data} として Lingara に送ります。種類は world.context_changed(scene、source_lang、target_lang、level、任意で name と persona を持つ npc、そして tags)か、world.practice_requested(topic と、同じ言語とレベル)です。Idempotency-Key は必須で、UUID のような最大 255 文字の表示可能な ASCII 文字列です。これがないと応答は 400 と idempotency_key_required になります。イベントごとに一度設定し、再試行では同じキーを送ってください。1つのキーは1つのイベントです。1日以内に同じキーで2回目のリクエストを送ると、本文が異なっていても最初の応答(バイト単位ではなく JSON として等しいもの)が返り、それ以降は以下のように同じイベントが返ります。受信イベントには署名がなく、アクセストークンが証明になります。描写するのは世界であって、プレイヤーではありません。scene、npc、topic、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}}'各テキストフィールドは表示可能な文字の1行で、前後の空白を除いた後に数えます。scene と topic は最大 160、npc.name は最大 32、npc.persona は最大 120 です。改行、タブなどの制御文字は拒否され、不可視文字や書式文字も同様です。方向の上書き、一部の文字体系や絵文字に必要な結合子以外のゼロ幅文字、タグブロック、私用文字がこれにあたります。tags には最大 24 文字の小文字の機械用トークンを最大 8 個入れられ、レッスンプランには届きません。level は 1 から 9 で、2つの言語は異なる必要があります。これらの制限を外れたリクエストは 400 で拒否され、イベントは記録されません。
"generate": true(world.practice_requested の既定値)の場合、トークンには lesson_plans:write も必要です。これがないとリクエストは 403 で拒否され、イベントは記録されません。ある場合、Lingara はレッスンプランを開始します。チェックも請求も直接作成する場合と同じで、202 応答の reaction が何が起きたかを示します。started で plan_status が generating なら、data.plan_id が応答の plan_id である lesson_plan.ready または lesson_plan.failed が、使っているすべての経路で後から届きます。partial または complete なら、プランはライブラリから取得されたもので今すぐ読めます。イベントは約束されません。届くこともありますが、待つ価値があるのは generating だけです。refused または failed でも、イベント自体は有効です。同じキーで再試行されることはないので、もう一度試すには新しいイベントを送ってください。
1日以内の再試行には最初の応答が返ります。それ以降の再試行は保存されたイベントから組み立て直されます。イベントが開始したプランは保持されますが、反応が拒否された理由は保持されません。そのため、遅い再試行が reaction failed と internal で応答することがあります。これは最初の結果が記録されなかったという意味で、プランが存在しないという意味ではありません。plan_id を保存していればそれでプランを読むか、新しいイベントを送ってください。
Tidewater Games: サーバーのないゲーム
架空のスタジオ Tidewater Games は、プレイヤーが夜市を探索する Godot ゲームを作っています。開発者は自分のマシンで、自分のクライアントを使ってゲームを動かしています。
プレイヤーが麺の屋台に入ります。ゲームは上の「Lingara にイベントを送る」のコマンドで、"generate": true 付きの world.context_changed を送信し、応答の plan_id を保持します。
plan_status が generating なら、ゲームはその plan_id の lesson_plan.ready が届くまで、ストリームを読むかフィードをポーリングします。その後、以下のように lesson_plans:read でプランを読みます。プランがすでに complete なら、すぐに読みます。
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"後にスタジオは HTTPS エンドポイントを持つ小さなサーバーを追加し、lesson_plan.ready 用に登録します。同じイベントが同じ id でそこに届き、それを読むゲームのコードは変わりません。
クライアントシークレットは決してゲームのビルドに含めてはいけません。プレイヤーのデバイス上にあるものは何でも読み取られる可能性があるからです。Lingara がプレイヤーに代わってのサインインに対応するまでは、プレイヤーのマシン上のゲームはスタジオ自身のサーバーと通信し、Lingara と直接通信するのは開発者自身のコピーだけです。
イベントの費用
クライアントの使用状況は、すべての /v1/ 呼び出しと同様に、受け付けた受信イベント、フィードの呼び出し、開いたストリームをそれぞれ1回として数えます。"generate": true 付きで送ったイベントは、レッスンプランとしても数えられます。Webhook の配信は、何回目の試行であっても最初の 2xx の時点で、イベントとエンドポイントの組ごとに1回数えられます。再び数えられることはなく、テストは数えられません。GET /v1/usage で今月のこれまでの使用状況を確認できます。
翻訳と英語版リファレンスの内容が異なる場合は、英語版リファレンスが正しいものとします。