Вы создали группу задач и за миллисекунды получили 202. Теперь переводы выполняются в фоне: по одной задаче на каждую локаль. Можно опрашивать каждую задачу, пока она не завершится, — но вряд ли вам захочется гонять цикл опроса только ради того, чтобы узнать, что немецкий уже готов. Гораздо лучше, чтобы сервер сам узнавал о готовности каждой локали в ту же секунду.
Именно для этого и нужен вебхук. Если при создании задач вы передаёте callbackUrl, Lingo отправляет результат POST-запросом на этот URL, как только задача достигает конечного состояния — один POST на каждую локаль, ровно в момент её готовности. Если локаль перевелась успешно, вы получите translation.completed с данными. Если локаль завершилась ошибкой, придёт translation.failed с ошибкой. В любом случае вы получите уведомление по каждому языку — без дополнительных запросов.
На этой странице разобраны оба payload и то, как с ними работать. Доставка подписывается и повторяется при сбоях — этот механизм общий с Развёртывание и подробно описан на странице проверка подписи вебхука, на которую мы будем ссылаться в нужных местах.
На этой странице
- Как работает доставка
- Payload успешного завершения
- Payload ошибки
- Как обрабатывать вебхук
- Когда доставка — не лучший вариант
Как работает доставка#
Каждая локаль в группе — это отдельная задача. Как только одна из них доходит до конечного состояния, её результат отдельно отправляется на ваш callbackUrl — Lingo не ждёт самую медленную локаль и не собирает всю группу в один вызов. Четырнадцать целевых локалей — это до четырнадцати POST-запросов, которые приходят по мере готовности каждого языка и в том порядке, в каком они завершаются.
Указать адрес назначения для конкретного запроса можно через callbackUrl при создании группы задач, либо задать URL по умолчанию для всей организации в дашборде — тогда его будут наследовать все группы. Значение callbackUrl, переданное в конкретном запросе, переопределяет значение по умолчанию на уровне организации для этой группы.
Только HTTPS
callbackUrl должен использовать HTTPS. URL на HTTP будет отклонён с ошибкой 400 при создании задачи — вебхук подписывается, и передавать подписанный payload по открытому протоколу просто бессмысленно.
По сети идут payload двух типов, и различаются они по полю type: translation.completed и translation.failed. В обоих есть идентификаторы задачи и группы, к которым они относятся, а также локаль, которую они несут, поэтому один обработчик может маршрутизировать события по type и обновлять нужную запись.
Корректно обрабатывайте неизвестные типы событий
Сейчас по сети приходят translation.completed и translation.failed. Но считайте, что этот набор открыт: обрабатывайте известные типы и игнорируйте остальные, чтобы будущий тип события не сломал уже развёрнутый обработчик.
Payload успешного завершения#
Когда задача успешно завершается, payload содержит переведённый 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 | Локаль, на которую переведён этот payload |
data | Переведённый контент, соответствующий структуре отправленного вами data |
Если задача выдала результат, это не считается ошибкой — поэтому задача, завершившаяся как completed_with_warnings (результат получен, но необязательный этап пайплайн не выполнился), всё равно доставляется как translation.completed, с пригодным для использования data. Вебхук сообщает вам, что локаль готова; предупреждения по отдельным шагам, объясняющие, почему этап был пропущен, находятся в отдельной задаче, которую вы запрашиваете по jobId, когда они вам понадобятся.
Payload ошибки#
Локаль может завершиться ошибкой — например, модель может превысить тайм-аут или все настроенные модели окажутся недоступны. Когда задача доходит до состояния failed, вы всё равно получаете уведомление. В этом случае тип payload — 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 | Понятное человеку описание ошибки |
Ошибка относится только к одной локали. Если вы отправили de, fr и ja, то ошибка ja придёт отдельным POST-запросом translation.failed, а de и fr придут как translation.completed — немецкий и французский переводы будут доставлены независимо от этого. Статус группы partial-failure status отражает такую смешанную картину. Чтобы восстановить неуспешную локаль, отправьте новую задачу только для неё с новым ключом идемпотентности.
Как обрабатывать вебхук#
Первая мысль скептически настроенного читателя здесь совершенно справедлива: мой обработчик делает реальную работу — пишет в базу данных, сбрасывает кэш, рассылает события подключённым клиентам, — разве это не продержит соединение открытым слишком долго и вебхук не упрётся в тайм-аут?
Именно так, поэтому не заставляйте Lingo ждать. Сначала верните 200, потом обрабатывайте. Сразу подтвердите получение и выполняйте основную работу уже после отправки ответа. Обработчик, который отвечает быстро, поддерживает стабильную доставку; обработчик, который блокируется на downstream-операциях, провоцирует лишний повтор.
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);
}
});Middleware verifyWebhook — единственная часть, которую эта страница не определяет. Каждая доставка подписывается по спецификации Standard Webhooks, так что вам не придётся разбираться в какой-то самодельной схеме. Как именно проверять подпись — и по какому графику идут повторы после ответа не-2xx — полностью описано на странице проверка подписи вебхука, общей с Развёртывание. Подключите этот middleware до того, как начнёте доверять payload: непроверенное тело запроса — это неаутентифицированное тело.
Сначала проверяйте подпись — потом доверяйте телу запроса
Ваш endpoint — это публичный URL, и отправить на него POST-запрос может кто угодно. Проверяйте подпись по исходному телу запроса, прежде чем что-либо делать с payload. Что именно для этого нужно — заголовки, HMAC и секрет whsec_ — описано на странице проверка подписи.
Когда доставка — не лучший вариант#
Вебхук — это удобный push-механизм, а не источник истины. В двух случаях лучше использовать другой инструмент, и оба — в одном клике отсюда.
Если в момент доставки результата ваш endpoint был недоступен, платформа повторит попытку — и даже если все повторы будут исчерпаны, результат не потеряется. Он по-прежнему будет доступен по jobId; поле callbackStatus у задачи фиксирует, удалось ли в итоге доставить результат. Сам график повторов описан на странице подпись и доставка. В обычном сценарии вебхук избавляет вас от цикла опроса; в редком сценарии под ним всегда остаётся запись о задаче.
А если вам нужен живой прогресс в UI — счётчик, который меняется с 3 из 14 на 4 из 14 по мере готовности локалей, а не отдельный callback на сервер для каждой локали, — тогда вам нужен WebSocket группы задач, а не вебхук.
