Creaste un grupo de trabajos y recibiste un 202 en milisegundos. Las traducciones ahora se están ejecutando en segundo plano, un trabajo por idioma. Podrías consultar cada trabajo hasta que termine, pero seguramente no quieres montar un ciclo de sondeo solo para enterarte de que alemán ya está listo. Lo que quieres es que tu servidor se entere en el momento en que cada idioma quede listo.
Para eso está el webhook. Cuando pasas un callbackUrl al crear trabajos, Lingo envía el resultado por POST a esa URL en cuanto cada trabajo alcanza un estado terminal: un POST por idioma, justo cuando queda listo. Si un idioma se traduce correctamente, llega como translation.completed con los datos. Si un idioma falla, llega como translation.failed con el error. Te enteras de cualquier forma, por idioma, sin tener que preguntar.
Esta página cubre los dos payloads y cómo manejarlos. La entrega va firmada y tiene reintentos; esa mecánica se comparte con provisioning y vive en la página de verificación de firma del webhook, enlazada en cada punto donde la vas a necesitar.
En esta página
- Cómo funciona la entrega
- El payload de completado
- El payload de error
- Cómo manejar un webhook
- Cuándo la entrega no es la herramienta adecuada
Cómo funciona la entrega#
Cada idioma de un grupo es un trabajo independiente. En cuanto uno alcanza un estado terminal, su resultado se entrega por separado a tu callbackUrl; Lingo no espera al idioma más lento ni agrupa todo en una sola llamada. Catorce idiomas de destino significan hasta catorce POST, que van llegando a medida que termina cada idioma, en el orden en que terminen.
Configura el destino por solicitud con callbackUrl cuando crees el grupo de trabajos, o define un valor predeterminado de la organización en el dashboard que todos los grupos heredan. Un callbackUrl por solicitud anula el valor predeterminado de la organización para ese grupo.
Solo HTTPS
callbackUrl debe usar HTTPS. Una URL HTTP se rechaza con un 400 cuando creas el trabajo: el webhook va firmado, y enviar un payload firmado sobre texto sin cifrar le quita todo el sentido.
Hay dos formas de payload que viajan por la red, distinguidas por su campo type: translation.completed y translation.failed. Ambas identifican el trabajo y el grupo al que pertenecen, además del idioma que traen, así que un solo handler puede enrutar según type y actualizar el registro correcto.
Maneja con elegancia los tipos de evento desconocidos
Hoy por la red viajan translation.completed y translation.failed. Toma ese conjunto como abierto: maneja los tipos que conoces e ignora el resto, para que un tipo de evento futuro no rompa un handler ya desplegado.
El payload de completado#
Cuando un trabajo termina correctamente, el payload trae el data traducido: la misma forma que obtendrías al consultar el trabajo, pero enviada a ti en lugar de tener que sondearla. El data refleja la estructura que enviaste: cada string traducido, cada valor que no es string (números, booleanos, null) preservado y la anidación intacta.
{
"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" }
}
}| Campo | Descripción |
|---|---|
type | translation.completed |
jobId | El trabajo que terminó (prefijo ljb_) |
groupId | El grupo al que pertenece (prefijo ljg_) |
sourceLocale | El idioma de origen que enviaste |
targetLocale | El idioma al que se tradujo este payload |
data | Contenido traducido, con la misma estructura del data que enviaste |
Un trabajo que produce output no es un fallo; por eso, un trabajo que terminó como completed_with_warnings (se produjo output, pero una etapa opcional del pipeline no se completó) se entrega como translation.completed, con un data utilizable. El webhook te avisa que el idioma está listo; las advertencias por paso que explican qué ocurrió viven en el trabajo individual, que recuperas por jobId cuando las necesites.
El payload de error#
Un idioma puede fallar: un modelo puede agotar el tiempo de espera, o todos los modelos configurados pueden no estar disponibles. Cuando un trabajo llega a failed, igual recibes la notificación. El tipo de payload es translation.failed y trae un string error en lugar de data:
{
"type": "translation.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "ja",
"error": "Model timeout after 30 seconds"
}| Campo | Descripción |
|---|---|
type | translation.failed |
jobId | El trabajo que falló |
groupId | El grupo al que pertenece |
sourceLocale | El idioma de origen que enviaste |
targetLocale | El idioma que falló |
error | Descripción del fallo en lenguaje claro |
El fallo se limita a un solo idioma. Si enviaste de, fr y ja, un fallo en ja se entrega como su propio POST translation.failed, mientras que de y fr llegan como translation.completed; las traducciones al alemán y al francés se entregan de todos modos. El estado de fallo parcial del grupo refleja esa mezcla. Para recuperar el idioma que falló, envía un trabajo nuevo solo para ese idioma con una clave de idempotencia nueva.
Cómo manejar un webhook#
La primera reacción de cualquier lector escéptico acá es la correcta: mi handler hace trabajo real —una escritura en base de datos, una invalidación de caché, una distribución a clientes conectados—, así que ¿eso no va a mantener la conexión abierta el tiempo suficiente como para que el webhook expire?
Sí, así que no hagas esperar a Lingo. Devuelve 200 primero y procesa después. Confirma la recepción de inmediato y deja el trabajo real para después de enviar la respuesta. Un handler que responde rápido mantiene la entrega saludable; un handler que bloquea por trabajo aguas abajo provoca un reintento que nunca necesitó.
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);
}
});El middleware verifyWebhook es la única pieza que esta página no define. Cada entrega va firmada según la especificación Standard Webhooks, así que no es un esquema que tengas que descifrar por tu cuenta. Cómo verificarla —y el calendario de reintentos detrás de una respuesta no 2xx— está documentado por completo en verificación de firma del webhook, compartido con provisioning. Conecta ese middleware antes de confiar en cualquier payload: un body sin verificar es un body no autenticado.
Verifica antes de confiar en el body
Tu endpoint es una URL pública; cualquiera puede hacerle POST. Verifica la firma contra el body sin procesar de la solicitud antes de actuar sobre cualquier payload. El cómo —headers, el HMAC, el secreto whsec_— está en la página de verificación de firma.
Cuándo la entrega no es la herramienta adecuada#
El webhook es una comodidad de tipo push, no el sistema de registro. Hay dos casos en los que conviene usar otra cosa, y ambos están a un clic de distancia.
Si tu endpoint estaba caído cuando se entregó un resultado, la plataforma reintenta; y si se agotan todos los reintentos, el resultado no se pierde. Sigue siendo recuperable por jobId; el callbackStatus del trabajo registra si el envío por push finalmente tuvo éxito. El calendario de reintentos en sí está en la página de firma y entrega. El webhook te ahorra un ciclo de sondeo en el caso común; el registro del trabajo siempre está ahí por debajo para el menos común.
Y si lo que quieres es progreso en vivo en una UI —un contador que pase de 3 de 14 a 4 de 14 a medida que van quedando listos los idiomas, en lugar de un callback por idioma a tu servidor—, entonces lo que necesitas es el WebSocket del grupo de trabajos, no el webhook.
