Criou um grupo de jobs e recebeu um 202 em milissegundos. As traduções estão agora a correr em segundo plano, um job por idioma. Podia consultar cada job até terminar — mas preferia não manter um ciclo de polling só para saber quando o alemão está pronto. O que quer é que o seu servidor seja notificado no momento em que cada idioma fica concluído.
É para isso que serve o webhook. Quando passa um callbackUrl ao criar jobs, a Lingo envia o resultado por POST para esse URL assim que cada job atinge um estado terminal — um POST por idioma, no exacto momento em que fica pronto. Um idioma traduzido sem problemas chega como translation.completed com os dados. Um idioma que falha chega como translation.failed com o erro. É notificado de qualquer dos casos, por idioma, sem ter de ir perguntar.
Esta página explica os dois formatos de payload e como lidar com eles. A entrega é assinada e repetida em caso de falha — esse mecanismo é partilhado com o aprovisionamento e está documentado na página de verificação da assinatura do webhook, com ligações nos pontos em que vai precisar dela.
Nesta página
- Como funciona a entrega
- O payload de conclusão
- O payload de falha
- Como tratar um webhook
- Quando a entrega não é a ferramenta certa
Como funciona a entrega#
Cada idioma num grupo é um job independente. No instante em que um deles atinge um estado terminal, o respetivo resultado é entregue ao seu callbackUrl de forma autónoma — a Lingo não espera pelo idioma mais lento, nem agrega o grupo numa única chamada. Catorze idiomas de destino significam até catorze POSTs, a chegar à medida que cada idioma termina, pela ordem em que terminarem.
Defina o destino por pedido com callbackUrl quando criar o grupo de jobs, ou defina uma predefinição da organização no dashboard que todos os grupos herdam. Um callbackUrl por pedido sobrepõe-se à predefinição da organização para esse grupo.
Apenas HTTPS
callbackUrl tem de usar HTTPS. Um URL HTTP é rejeitado com um 400 quando cria o job — o webhook é assinado, e um payload assinado sobre texto simples anula o propósito.
Há dois formatos de payload a circular, distinguidos pelo campo type: translation.completed e translation.failed. Ambos identificam o job, o grupo a que pertencem e o idioma a que dizem respeito, para que um único handler possa encaminhar com base em type e atualizar o registo certo.
Trate tipos de evento desconhecidos com elegância
Hoje, o sistema transporta translation.completed e translation.failed. Trate este conjunto como aberto — faça branch nos tipos que conhece e ignore os restantes, para que um futuro tipo de evento não possa quebrar um handler já em produção.
O payload de conclusão#
Quando um job termina com sucesso, o payload transporta o data traduzido — com o mesmo formato que obteria ao ir buscar o job, mas enviado para si em vez de ser obtido por polling. O data espelha a estrutura que submeteu: todas as strings traduzidas, todos os valores não textuais (números, booleanos, null) preservados, com o aninhamento intacto.
{
"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 | Descrição |
|---|---|
type | translation.completed |
jobId | O job que terminou (prefixo ljb_) |
groupId | O grupo a que pertence (prefixo ljg_) |
sourceLocale | O idioma de origem que submeteu |
targetLocale | O idioma para o qual este payload foi traduzido |
data | Conteúdo traduzido, com a mesma estrutura do data que submeteu |
Um job que produz output não é uma falha — por isso, um job que terminou como completed_with_warnings (houve output, mas uma etapa opcional do pipeline falhou) é entregue como translation.completed, com data utilizável. O webhook diz-lhe que o idioma está pronto; os avisos por etapa que explicam essa falha estão no job individual, que pode obter por jobId quando precisar deles.
O payload de falha#
Um idioma pode falhar — um modelo pode exceder o tempo limite, ou todos os modelos configurados podem estar indisponíveis. Quando um job atinge failed, continua a ser notificado. O tipo de payload é translation.failed e traz uma string error em vez de data:
{
"type": "translation.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "ja",
"error": "Model timeout after 30 seconds"
}| Campo | Descrição |
|---|---|
type | translation.failed |
jobId | O job que falhou |
groupId | O grupo a que pertence |
sourceLocale | O idioma de origem que submeteu |
targetLocale | O idioma que falhou |
error | Descrição legível da falha |
A falha está limitada a um único idioma. Se submeteu de, fr e ja, uma falha em ja é entregue no seu próprio POST translation.failed, enquanto de e fr chegam como translation.completed — as traduções em alemão e francês seguem na mesma. O estado de falha parcial do grupo reflete essa combinação. Para recuperar o idioma que falhou, submeta um novo job apenas para esse idioma, com uma nova chave de idempotência.
Como tratar um webhook#
A primeira reação de um leitor mais cético aqui é a certa: o meu handler faz trabalho real — escreve na base de dados, invalida a cache, faz fan-out para clientes ligados — por isso, não vai manter a ligação aberta tempo suficiente para o webhook expirar?
Vai, por isso não faça a Lingo esperar. Devolva primeiro 200; processe depois. Acuse a receção imediatamente e faça o trabalho real depois de enviar a resposta. Um handler que responde depressa mantém a entrega saudável; um handler que fica bloqueado em trabalho a jusante provoca uma repetição de que não precisava.
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);
}
});O middleware verifyWebhook é a única peça que esta página não define. Cada entrega é assinada de acordo com a especificação Standard Webhooks, por isso não é um esquema que tenha de decifrar por engenharia inversa. A forma de a verificar — e o calendário de repetições após uma resposta não 2xx — está documentada por completo em verificação da assinatura do webhook, partilhada com o aprovisionamento. Ligue esse middleware antes de confiar num payload: um body não verificado é um body não autenticado.
Verifique antes de confiar no body
O seu endpoint é um URL público; qualquer pessoa lhe pode fazer um POST. Verifique a assinatura com base no body bruto do pedido antes de agir sobre qualquer payload. O como — cabeçalhos, o HMAC, o segredo whsec_ — está na página de verificação da assinatura.
Quando a entrega não é a ferramenta certa#
O webhook é uma conveniência de push, não o sistema de registo. Há dois casos em que precisa de outra coisa, e ambos estão à distância de um clique.
Se o seu endpoint estiver em baixo quando um resultado for entregue, a plataforma repete a tentativa — e, se todas as tentativas se esgotarem, o resultado não se perde. Continua recuperável por jobId; o callbackStatus do job regista se o envio acabou por ter sucesso. O próprio calendário de repetições está na página de assinatura e entrega. O webhook poupa-lhe um ciclo de polling no caso comum; o registo do job está sempre lá por trás, para o caso menos comum.
E se o que pretende é progresso em tempo real numa interface — um contador a passar de 3 em 14 para 4 em 14 à medida que os idiomas ficam prontos, em vez de um callback por idioma para o seu servidor — então o que procura é o WebSocket do grupo de jobs, não o webhook.
