Vous avez créé un groupe de tâches et reçu un 202 en quelques millisecondes. Les traductions s'exécutent maintenant en arrière-plan, à raison d'une tâche par langue. Vous pourriez interroger chaque tâche jusqu'à ce qu'elle se termine, mais vous préférez éviter de faire tourner une boucle de polling juste pour savoir quand l'allemand est prêt. Vous voulez que votre serveur soit prévenu dès qu'une langue est disponible.
C'est exactement le rôle du webhook. Lorsque vous transmettez un callbackUrl au moment de créer les tâches, Lingo envoie le résultat par POST à cette URL dès qu'une tâche atteint un état terminal : un POST par langue, dès qu'elle est prête. Une langue traduite sans incident arrive sous la forme translation.completed avec les données. Une langue en échec arrive sous la forme translation.failed avec l'erreur. Vous êtes informé dans les deux cas, pour chaque langue, sans avoir à venir le demander.
Cette page présente les deux formats de payload et la façon de les gérer. L'envoi est signé et fait l'objet de nouvelles tentatives : ce mécanisme est partagé avec le provisioning et détaillé sur la page vérification de la signature des webhooks, vers laquelle nous renvoyons à chaque fois que nécessaire.
Sur cette page
- Comment l'envoi fonctionne
- Le payload completed
- Le payload failed
- Gérer un webhook
- Quand l'envoi n'est pas le bon outil
Comment l'envoi fonctionne#
Chaque langue d'un groupe correspond à une tâche indépendante. Dès que l'une d'elles atteint un état terminal, son résultat est envoyé individuellement à votre callbackUrl : Lingo n'attend pas la langue la plus lente et ne regroupe pas tout le groupe en un seul appel. Quatorze langues cibles, c'est jusqu'à quatorze POST, reçus à mesure que chaque langue se termine, dans l'ordre où elles arrivent.
Définissez la destination pour chaque requête avec callbackUrl lorsque vous créez le groupe de tâches, ou configurez une valeur par défaut au niveau de l'organisation dans le tableau de bord, dont chaque groupe héritera. Un callbackUrl défini à la requête remplace la valeur par défaut de l'organisation pour ce groupe.
HTTPS uniquement
callbackUrl doit utiliser HTTPS. Une URL HTTP est rejetée avec une erreur 400 lorsque vous créez la tâche : le webhook est signé, et envoyer un payload signé en clair irait à l'encontre de l'objectif.
Deux formats de payload transitent sur le réseau, distingués par leur champ type : translation.completed et translation.failed. Tous deux indiquent la tâche et le groupe auxquels ils appartiennent, ainsi que la langue concernée, afin qu'un seul handler puisse s'aiguiller sur type et mettre à jour le bon enregistrement.
Gérez proprement les types d'événement inconnus
Aujourd'hui, le flux transporte translation.completed et translation.failed. Considérez cet ensemble comme ouvert : gérez les types que vous connaissez et ignorez le reste, afin qu'un futur type d'événement ne puisse pas casser un handler déjà déployé.
Le payload completed#
Lorsqu'une tâche se termine avec succès, le payload contient le data traduit : c'est la même structure que celle obtenue en récupérant la tâche, mais poussée vers vous au lieu d'être récupérée par polling. Le data reflète la structure que vous avez soumise : chaque chaîne est traduite, chaque valeur non textuelle (nombres, booléens, null) est conservée, et l'imbrication reste intacte.
{
"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" }
}
}| Champ | Description |
|---|---|
type | translation.completed |
jobId | La tâche terminée (préfixe ljb_) |
groupId | Le groupe auquel elle appartient (préfixe ljg_) |
sourceLocale | La langue source que vous avez soumise |
targetLocale | La langue dans laquelle ce payload a été traduit |
data | Contenu traduit, conforme à la structure du data que vous avez soumis |
Une tâche qui produit un résultat n'est pas un échec. Ainsi, une tâche terminée avec l'état completed_with_warnings (résultat produit, mais avec une étape facultative du pipeline qui n'a pas abouti) est envoyée sous la forme translation.completed, avec un data exploitable. Le webhook vous indique que la langue est prête ; les avertissements détaillés par étape qui expliquent ce contournement se trouvent sur la tâche individuelle, que vous récupérez via jobId lorsque vous en avez besoin.
Le payload failed#
Une langue peut échouer : un modèle peut expirer, ou tous les modèles configurés peuvent être indisponibles. Lorsqu'une tâche atteint failed, vous en êtes tout de même informé. Le type de payload est translation.failed, et il contient une chaîne error à la place de data :
{
"type": "translation.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "ja",
"error": "Model timeout after 30 seconds"
}| Champ | Description |
|---|---|
type | translation.failed |
jobId | La tâche en échec |
groupId | Le groupe auquel elle appartient |
sourceLocale | La langue source que vous avez soumise |
targetLocale | La langue en échec |
error | Description de l'échec en clair |
L'échec est limité à une seule langue. Si vous avez soumis de, fr et ja, un échec sur ja est envoyé dans son propre POST translation.failed, tandis que de et fr arrivent sous forme de translation.completed : les traductions allemande et française sont bien livrées. Le statut d'échec partiel du groupe reflète ce mélange. Pour relancer la langue en échec, soumettez une nouvelle tâche pour cette seule langue avec une nouvelle clé d'idempotence.
Gérer un webhook#
La première réaction d'un lecteur sceptique ici est la bonne : mon handler effectue un vrai travail — écriture en base de données, invalidation de cache, diffusion vers des clients connectés — donc est-ce que ça ne va pas garder la connexion ouverte assez longtemps pour faire expirer le webhook ?
Si, donc ne faites pas attendre Lingo. Renvoyez d'abord 200, puis traitez. Accusez réception immédiatement, puis effectuez le vrai travail une fois la réponse envoyée. Un handler qui répond vite garantit un envoi sain ; un handler qui bloque sur du travail en aval déclenche des tentatives de renvoi inutiles.
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);
}
});Le middleware verifyWebhook est le seul élément que cette page ne définit pas. Chaque envoi est signé conformément à la spécification Standard Webhooks, vous n'avez donc pas à en rétroconcevoir le fonctionnement. La façon de le vérifier — ainsi que le calendrier des nouvelles tentatives après une réponse non 2xx — est entièrement documentée sur la page vérification de la signature des webhooks, partagée avec le provisioning. Intégrez ce middleware avant de faire confiance à un payload : un corps non vérifié est un corps non authentifié.
Vérifiez avant de faire confiance au corps
Votre endpoint est une URL publique ; n'importe qui peut lui envoyer un POST. Vérifiez la signature à partir du corps brut de la requête avant d'agir sur le moindre payload. La procédure — en-têtes, HMAC, secret whsec_ — est décrite sur la page vérification de signature.
Quand l'envoi n'est pas le bon outil#
Le webhook est un mécanisme push pratique, pas le système de référence. Deux cas appellent autre chose, et les deux sont à un clic.
Si votre endpoint était indisponible au moment où un résultat a été envoyé, la plateforme effectue de nouvelles tentatives. Et si elles échouent toutes, le résultat n'est pas perdu. Il reste récupérable par jobId ; le callbackStatus de la tâche indique si l'envoi a finalement réussi. Le calendrier des nouvelles tentatives figure sur la page signature et envoi. Le webhook vous évite une boucle de polling dans le cas courant ; dans le cas moins fréquent, la tâche reste toujours la source sous-jacente.
Et si vous voulez afficher une progression en direct dans une interface — un compteur qui passe de 3 sur 14 à 4 sur 14 à mesure que les langues arrivent, plutôt qu'un callback par langue vers votre serveur — il vous faut le WebSocket du groupe de tâches, pas le webhook.
