Has creado un grupo de trabajos y has recibido un 202 en milisegundos. Las traducciones se están ejecutando ahora en segundo plano, un trabajo por idioma. Podrías consultar cada trabajo hasta que termine, pero seguramente no quieras montar un bucle de sondeo solo para saber que el alemán ya está listo. Lo que quieres es que tu servidor se entere en cuanto llegue cada idioma.
Para eso está el webhook. Cuando pasas un callbackUrl al crear trabajos, Lingo.dev envía el resultado por POST a esa URL en cuanto cada trabajo alcanza un estado terminal: un POST por idioma, en el mismo momento en que está listo. Un idioma que se traduce sin problemas llega como translation.completed con los datos. Un idioma que falla llega como translation.failed con el error. Se te notifica en cualquier caso, por idioma, sin tener que preguntar.
Esta página cubre las dos cargas útiles y cómo gestionarlas. La entrega va firmada y se reintenta; ese mecanismo se comparte con el aprovisionamiento y está documentado en la página de verificación de la firma del webhook, enlazada en cada punto en el que la necesites.
En esta página
- Cómo funciona la entrega
- La carga útil completada
- La carga útil fallida
- Cómo gestionar 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 el instante en que uno alcanza un estado terminal, su resultado se entrega por separado a tu callbackUrl: Lingo.dev no espera al idioma más lento ni agrupa todo el grupo 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 para la organización en el panel que heredarán todos los grupos. 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 al crear el trabajo: el webhook va firmado, y enviar una carga útil firmada sobre texto sin cifrar va justo en contra de su propósito.
Por la red circulan dos formatos de carga útil, diferenciados por su campo type: translation.completed y translation.failed. Ambos indican el trabajo y el grupo al que pertenecen, así como el idioma que llevan, de modo que un único controlador pueda enrutar según type y actualizar el registro correcto.
Gestiona con elegancia los tipos de evento desconocidos
Hoy por hoy, por la red viajan translation.completed y translation.failed. Trata ese conjunto como abierto: ramifica según los tipos que conozcas e ignora el resto, para que un futuro tipo de evento no rompa un controlador ya desplegado.
La carga útil completada#
Cuando un trabajo termina correctamente, la carga útil incluye el data traducido: la misma estructura que obtendrías al recuperar el trabajo, pero enviada a ti en lugar de consultarla por sondeo. El data refleja la estructura que enviaste: cada cadena traducida, cada valor no textual (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 ha traducido esta carga útil |
data | Contenido traducido, con la misma estructura que el data que enviaste |
Un trabajo que genera salida no es un fallo. Por eso, un trabajo que termina como completed_with_warnings (se generó salida, pero una fase opcional del pipeline se quedó por el camino) se entrega como translation.completed, con un data utilizable. El webhook te avisa de que el idioma está listo; las advertencias por paso que explican lo ocurrido están en el trabajo individual, que puedes recuperar por jobId cuando las necesites.
La carga útil fallida#
Un idioma puede fallar: un modelo puede agotar el tiempo de espera, o todos los modelos configurados pueden no estar disponibles. Cuando un trabajo alcanza failed, se te sigue notificando. El tipo de carga útil es translation.failed y contiene una cadena 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 natural |
El fallo se limita a un solo idioma. Si enviaste de, fr y ja, un fallo de ja se entrega en 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 igualmente. El estado de fallo parcial del grupo refleja esa combinación. Para recuperar el idioma fallido, envía un nuevo trabajo solo para ese idioma con una clave de idempotencia nueva.
Cómo gestionar un webhook#
La primera idea de una persona escéptica al leer esto es la correcta: mi controlador hace trabajo real —una escritura en base de datos, una invalidación de caché, una distribución a clientes conectados—, así que ¿no mantendrá la conexión abierta el tiempo suficiente como para que el webhook agote el tiempo de espera?
Sí. Así que no hagas que Lingo.dev espere. Devuelve primero un 200 y procesa después. Confirma la recepción de inmediato y haz el trabajo real una vez enviada la respuesta. Un controlador que responde rápido mantiene la entrega en buen estado; uno que se queda bloqueado por trabajo aguas abajo provoca un reintento innecesario.
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 siguiendo la especificación Standard Webhooks, así que no es un esquema que tengas que deducir por ingeniería inversa. Cómo verificarlo —y la cadencia de reintentos tras una respuesta no 2xx— está documentado por completo en verificación de la firma del webhook, compartida con el aprovisionamiento. Conecta ese middleware antes de confiar en una carga útil: un cuerpo no verificado es un cuerpo no autenticado.
Verifica antes de confiar en el cuerpo
Tu endpoint es una URL pública; cualquiera puede hacerle POST. Verifica la firma con el cuerpo en bruto de la solicitud antes de actuar sobre cualquier carga útil. El cómo —cabeceras, 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 basada en push, no el sistema de referencia. Hay dos casos en los que conviene usar otra cosa, y ambos están a un clic.
Si tu endpoint estaba caído cuando se entregó un resultado, la plataforma reintenta el envío; y si se agotan todos los reintentos, el resultado no se pierde. Sigue siendo recuperable mediante jobId; el callbackStatus del trabajo registra si ese push acabó teniendo éxito. La propia cadencia de reintentos está en la página de firma y entrega. El webhook te ahorra un bucle de sondeo en el caso habitual; el registro del trabajo siempre está ahí debajo en el caso menos común.
Y si lo que quieres es progreso en tiempo real en una UI —un contador que pase de 3 de 14 a 4 de 14 a medida que van llegando idiomas, en lugar de una devolución de llamada por idioma a tu servidor—, entonces lo que necesitas es el WebSocket del grupo de trabajos, no el webhook.
