ジョブグループを作成すると、数ミリ秒で 202 が返ります。翻訳はその後バックグラウンドで進み、ロケールごとに1つずつジョブが動きます。完了まで各ジョブをポーリングし続けることもできますが、ドイツ語の準備ができたことを知るためだけにポーリングループを回したくはないはずです。欲しいのは、各ロケールが仕上がった瞬間にサーバーへ通知が飛ぶことです。
それを実現するのが webhook です。ジョブ作成時に callbackUrl を渡すと、Lingo.dev は各ジョブが終端状態に達した時点で、その URL に結果を POST します。ロケールごとに1回、完了したその瞬間に POST されます。 問題なく翻訳できたロケールは、データ付きの translation.completed として届きます。失敗したロケールは、エラー付きの translation.failed として届きます。こちらから取りにいかなくても、言語ごとに必ず通知されます。
このページでは、2種類のペイロードとその扱い方を説明します。配信には署名と再試行があり、その仕組みはプロビジョニングと共通です。詳細は webhook署名の検証 ページにまとまっており、必要な箇所からすぐ辿れるようになっています。
このページの内容
配信の仕組み#
グループ内の各ロケールは、それぞれ独立したジョブです。どれか1つが終端状態に達した瞬間、その結果だけが個別に callbackUrl へ配信されます。Lingo.dev は最も遅いロケールを待ちませんし、グループ全体をまとめて1回の呼び出しにすることもありません。対象ロケールが14件なら、最大14回の POST が、それぞれの言語の完了に合わせて、完了した順に届きます。
送信先は、ジョブグループを作成するときに callbackUrl でリクエストごとに指定できます。あるいは、ダッシュボードで組織のデフォルトを設定して、すべてのグループに継承させることもできます。リクエスト単位で指定した callbackUrl は、そのグループでは組織のデフォルトより優先されます。
HTTPS のみ
callbackUrl には HTTPS を使う必要があります。HTTP の URL はジョブ作成時に 400 で拒否されます。webhook は署名付きなので、平文通信で送ってしまうと意味がありません。
送られてくるペイロードは2種類で、見分けるキーは type フィールドです。種類は translation.completed と translation.failed。どちらにも、対象のジョブ、所属グループ、対象ロケールが含まれるので、1つのハンドラーで type を見て振り分け、正しいレコードを更新できます。
未知のイベント型も安全に扱う
現時点で送られてくるのは translation.completed と translation.failed です。ただし、この種類は今後増える前提で扱ってください。既知の型だけを分岐し、それ以外は無視するようにしておけば、将来イベント型が追加されても、デプロイ済みのハンドラーが壊れません。
完了時のペイロード#
ジョブが正常に完了すると、ペイロードには翻訳済みの data が入ります。形式は ジョブを取得 したときと同じで、ポーリングで取りにいく代わりにプッシュで届くイメージです。data は、送信した構造をそのまま保っています。すべての文字列は翻訳され、文字列以外の値(数値、真偽値、null)はそのまま保持され、ネストも崩れません。
{
"type": "translation.completed",
"jobId": "ljb_A1b2C3d4E5f6G7h8",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "de",
"data": {
"id": "course_101",
"title": "Einführung in maschinelles Lernen",
"steps": [
{ "heading": "Was ist ML?", "body": "Maschinelles Lernen ist ein Teilbereich der künstlichen Intelligenz." },
{ "heading": "Überwachtes Lernen", "body": "Trainieren eines Modells mit gelabelten Daten." }
],
"metadata": { "author": "Dr. Smith", "difficulty": "beginner" }
}
}| フィールド | 説明 |
|---|---|
type | translation.completed |
jobId | 完了したジョブ(ljb_ プレフィックス) |
groupId | 所属するグループ(ljg_ プレフィックス) |
sourceLocale | 送信したソースロケール |
targetLocale | このペイロードの翻訳先ロケール |
data | 送信した data と同じ構造の翻訳済みコンテンツ |
出力が生成されているジョブは失敗扱いではありません。つまり、completed_with_warnings で完了したジョブ(出力は生成されたものの、任意の pipeline ステージを通り切らなかったケース)も、利用可能な data を含む translation.completed として配信されます。webhook が伝えるのは、そのロケールが使える状態になったということです。どのステップで何が起きたかという警告は 単一ジョブ 側にあり、必要になったときに jobId で取得します。
失敗時のペイロード#
ロケール単位では失敗することもあります。たとえばモデルがタイムアウトしたり、設定したすべてのモデルが利用できなかったりする場合です。ジョブが failed に達した場合でも、通知は届きます。ペイロードの型は translation.failed で、data の代わりに error 文字列が入ります。
{
"type": "translation.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "ja",
"error": "Model timeout after 30 seconds"
}| フィールド | 説明 |
|---|---|
type | translation.failed |
jobId | 失敗したジョブ |
groupId | 所属するグループ |
sourceLocale | 送信したソースロケール |
targetLocale | 失敗したロケール |
error | 人が読んで分かる失敗内容の説明 |
失敗は1つのロケールに閉じます。たとえば de、fr、ja を送っていた場合、ja の失敗は、それ単体の translation.failed POST として配信される一方、de と fr は translation.completed として届きます。つまり、ドイツ語とフランス語の翻訳はそのまま配信されます。グループの partial-failure status には、この混在した状態が反映されます。失敗したロケールをやり直すには、新しい冪等キーを使って、そのロケールだけの新しいジョブを送信してください。
webhook の処理#
ここでまず気になるのはもっともです。ハンドラー側では、DB への書き込みやキャッシュの削除、接続中クライアントへのファンアウトなど、実処理が走る。となると、そのせいで接続を開きっぱなしにして webhook をタイムアウトさせてしまわないか?
そのとおりです。だからこそ、Lingo.dev を待たせてはいけません。まず 200 を返し、そのあとで処理する。 受信確認はすぐ返し、実際の処理はレスポンス送信後に行ってください。すぐ応答を返すハンドラーは配信の健全性を保てますが、下流処理でブロックすると、本来不要な再試行を招きます。
app.post("/webhooks/translations", verifyWebhook, async (req, res) => {
// Acknowledge first - one POST per locale, the moment it lands.
res.status(200).send("ok");
const { type, jobId, groupId, targetLocale, data } = req.body;
if (type === "translation.completed") {
await db.content.update({
where: { groupId },
data: { [`content_${targetLocale}`]: data },
});
// Advance your own progress model - your UI can poll this or receive it over SSE.
await db.translationProgress.increment({
where: { groupId },
data: { completedLanguages: { increment: 1 } },
});
}
if (type === "translation.failed") {
console.error(`Translation failed: ${jobId} (${targetLocale})`, req.body.error);
}
});このページで扱っていない唯一の要素が verifyWebhook ミドルウェアです。すべての配信は Standard Webhooks 仕様に従って署名されているため、独自仕様を読み解く必要はありません。検証方法や、2xx 以外を返した場合の再試行スケジュールについては、プロビジョニングと共通の webhook署名の検証 にまとまっています。ペイロードを信頼する前に、まずこのミドルウェアを組み込んでください。検証していないボディは、認証されていないボディです。
ボディを信頼する前に検証する
エンドポイントは公開 URL なので、誰でも POST できます。どんなペイロードでも処理する前に、必ず生のリクエストボディに対して署名を検証してください。手順、ヘッダー、HMAC、そして whsec_ シークレットについては、署名の検証 ページで説明しています。
配信が向いていないケース#
webhook は便利なプッシュ手段であって、システムの記録元ではありません。別の手段を使うべきケースが2つあり、どちらもリンク先ですぐ確認できます。
結果配信のタイミングでエンドポイントが落ちていても、プラットフォームは再試行します。さらに、すべての再試行を使い切っても、結果が失われることはありません。結果は引き続き jobId で取得可能 で、ジョブの callbackStatus には最終的にプッシュが成功したかどうかも記録されます。再試行スケジュール自体は 署名と配信 ページにあります。通常時には webhook がポーリングループを省いてくれますが、例外時にも、その下には常にジョブレコードがあります。
一方、欲しいのが UI 上でのリアルタイム進捗表示、つまりロケールが完了するたびに 14 件中 3 件から 4 件へとカウンターが進むような体験であって、サーバーへのロケール単位のコールバックではないなら、使うべきなのは webhook ではなくジョブグループ WebSocket です。
