|
Knowledgebase
EnterprisePlatform
PlatformAPI
React (MCP)React (Lingo Compiler)
Alpha
CLIIntegrations
GuidesChangelog

Synchronous

  • How it works
  • Localize
  • Recognize
  • ProvisionEnterprise

AsynchronousEnterprise

  • How it works
  • Queue
  • Webhooks
  • WebSocket

Webhooks

Max PrilutskiyMax Prilutskiy·Updated 1 day ago·3 min read

When an async localization job completes or fails, the platform POSTs the result to your webhook URL. Each language is delivered independently the moment it finishes.

Webhook URL configuration#

Configure a default webhook URL on your organization in the dashboard. This URL receives callbacks for all jobs across all groups. Individual requests can override the default by passing callbackUrl when creating jobs.

HTTPS required

Webhook URLs must use HTTPS. The platform rejects HTTP callback URLs.

Payload#

Completed#

json
{
  "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" }
  }
}

Failed#

For failed jobs, the payload type is translation.failed with an error field instead of data.

json
{
  "type": "translation.failed",
  "jobId": "ljb_C3d4E5f6G7h8I9j0",
  "groupId": "ljg_A1b2C3d4E5f6G7h8",
  "sourceLocale": "en",
  "targetLocale": "ja",
  "error": "Model timeout after 30 seconds"
}

Signature verification#

Every webhook includes three headers for signature verification, following the Standard Webhooks specification:

HeaderDescription
webhook-idThe job ID (unique per message)
webhook-timestampUnix timestamp (seconds) when the webhook was sent
webhook-signaturev1,{base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))}

The webhook secret is auto-generated for your organization the first time you submit a job with a callback URL. It uses the whsec_ prefix followed by base64-encoded key bytes.

Verify the signature using your organization's webhook secret:

javascript
import crypto from "node:crypto";

function verifyWebhook(payload, headers, secret) {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];

  // Reject timestamps older than 5 minutes (replay prevention)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    throw new Error("Webhook timestamp too old");
  }

  // Compute expected signature
  const content = `${msgId}.${timestamp}.${payload}`;
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");
  const expected = crypto
    .createHmac("sha256", secretBytes)
    .update(content)
    .digest("base64");

  // Verify (constant-time comparison)
  for (const sig of signatures.split(" ")) {
    const [version, value] = sig.split(",", 2);
    if (version === "v1" && crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(value)
    )) {
      return JSON.parse(payload);
    }
  }

  throw new Error("Invalid webhook signature");
}

Preserve the raw body

Compute the signature from the raw HTTP body before JSON parsing. If your framework auto-parses the body, whitespace or key ordering differences will break verification.

Retry behavior#

If your endpoint returns a non-2xx status code or is unreachable, the platform retries with exponential backoff starting at 30 seconds, up to 5 attempts. After all retries are exhausted, the webhook is marked as failed. You can still retrieve the result by polling GET /jobs/localization/:jobId.

Handling callbacks#

Return 200 immediately from your webhook handler, then process the translation asynchronously. This prevents webhook timeouts and ensures reliable delivery.

javascript
app.post("/webhooks/translations", verifyWebhook, async (req, res) => {
  // Return 200 first - process in background
  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 },
    });

    // Update progress - your UI can poll this or receive it via SSE
    await db.translationProgress.increment({
      where: { groupId },
      data: { completedLanguages: { increment: 1 } },
    });
  }

  if (type === "translation.failed") {
    console.error(`Translation failed: ${jobId}`, req.body.error);
  }
});

Next Steps#

Queue
Create localization jobs and poll status
WebSocket
Stream real-time progress updates to your UI
How it works
Overview of the async localization API

Was this page helpful?