🎉 v1.0

Get started

  • Welcome
  • Documentation
  • Pricing
    Soon

Tools

  • I18n MCP
  • CLI
  • CI/CD Integrations
  • Compiler
    Alpha
  • Connect Your Engine

Resources

  • Languages
  • LLM Models
  • Guides

Company

  • Enterprise
  • CareersHiring!
Dashboard

Lingo.dev

  • How it works
  • Changelognew
  • API
  • MCP

Localization Engine

  • Overview
  • Brand Voices
  • Instructions
  • Glossaries
  • LLM Models

Quality

  • Reports
  • AI Reviewers
  • Playground

Admin

  • API Keys
  • Team

Async Localization API

Max PrilutskiyMax Prilutskiy·Updated about 19 hours ago·8 min read

The async localization API is purpose-built for applications where content needs to be available in many languages and users expect to see each translation appear the moment it's ready. Submit a translation job, get a 202 back in milliseconds, and show a progress indicator in your UI while each language arrives independently via webhook.

Enterprise feature

The async localization API is available on Enterprise plans. Contact us to enable it for your organization.

The problem#

Training platforms, content management systems, and e-learning tools often need to translate content into dozens of languages the moment it's created or updated. The synchronous /process/localize endpoint works, but creates friction at scale.

Consider a training module authored in English that needs to reach learners in 14 languages. You have two options with the synchronous API:

  1. Parallel calls - Fire 14 requests simultaneously, one per target language. Each carries the same source payload. You can display each language as it arrives, but you're making 14 network round-trips with redundant data.

  2. Batch call - Use batchLocalizeText in the SDK to translate all languages in one request. Fewer network calls, but you wait for all 14 to finish before displaying any of them.

Both approaches tie up your application while translations process. If your server restarts, translations in flight are lost. If one language fails in a batch, you need retry logic for partial failures. And neither gives your end users a clean way to see that translations are in progress.

The async API eliminates these tradeoffs. Submit one job per language, get a 202 back in milliseconds, and wire the job status into your UI as a progress indicator. Each language arrives independently via webhook the moment it completes. Your application stays responsive. Your users see translations appearing in real time. The platform handles retries.

How it works#

1

Submit a job

POST to /translation-jobs with your content and target language. The API validates the request, creates a job record, and returns 202 with a job ID. Your application is free to continue immediately.

2

Background processing

The platform queues the job and processes it through your localization engine. It applies the same model selection, glossary rules, brand voice, and instructions as the synchronous API. The job status moves from queued to processing to completed or failed.

3

Receive results

When translation completes, the platform POSTs the result to your webhook URL. Each job is independent - if you submitted 14 languages, you receive 14 webhook callbacks as each one finishes. Alternatively, poll GET /translation-jobs/:id for status.

Authentication#

Same as the synchronous API. Pass your API key in the X-API-Key header. API keys are scoped to an organization and have access to all localization engines within it. See API Keys for details.

Create a translation job#

text
POST /translation-jobs
ParameterTypeDescription
sourceLocalestringBCP-47 source locale (e.g., en)
targetLocalestringBCP-47 target locale (e.g., de)
dataobjectKey-value pairs to translate
hintsobject (optional)Contextual hints per key (array of breadcrumb strings)
callbackUrlstring (optional)Webhook URL for this job. Overrides the organization default.
idempotencyKeystring (optional)Client-generated UUID to prevent duplicate jobs.

Request#

json
{
  "sourceLocale": "en",
  "targetLocale": "de",
  "data": {
    "lesson_title": "Introduction to Machine Learning",
    "lesson_summary": "This lesson covers the fundamentals of ML, including supervised and unsupervised learning."
  },
  "callbackUrl": "https://your-app.com/webhooks/translations",
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000"
}

Response (202 Accepted)#

json
{
  "id": "tjb_A1b2C3d4E5f6G7h8",
  "status": "queued",
  "createdAt": "2026-03-16T10:30:00.000Z"
}

Examples#

javascript
// Translate to 3 languages in parallel - fire and forget
const languages = ["de", "fr", "ja"];

const jobs = await Promise.all(
  languages.map((targetLocale) =>
    fetch("https://api.lingo.dev/translation-jobs", {
      method: "POST",
      headers: {
        "X-API-Key": "your_api_key",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        sourceLocale: "en",
        targetLocale,
        data: {
          lesson_title: "Introduction to Machine Learning",
          lesson_summary: "This lesson covers the fundamentals...",
        },
        callbackUrl: "https://your-app.com/webhooks/translations",
      }),
    }).then((r) => r.json())
  )
);

// All 3 calls return 202 in milliseconds.
// Results arrive via webhook as each language completes.
console.log(jobs.map((j) => j.id));
// ["tjb_A1b2C3d4E5f6G7h8", "tjb_B2c3D4e5F6g7H8i9", "tjb_C3d4E5f6G7h8I9j0"]

Check job status#

Poll this endpoint to check progress. Useful for CLI tools, debugging, or environments where you cannot expose a webhook endpoint.

text
GET /translation-jobs/:id

Response#

json
{
  "id": "tjb_A1b2C3d4E5f6G7h8",
  "status": "completed",
  "sourceLocale": "en",
  "targetLocale": "de",
  "outputData": {
    "lesson_title": "Einführung in maschinelles Lernen",
    "lesson_summary": "Diese Lektion behandelt die Grundlagen des ML, einschließlich überwachtem und unüberwachtem Lernen."
  },
  "createdAt": "2026-03-16T10:30:00.000Z",
  "startedAt": "2026-03-16T10:30:01.000Z",
  "completedAt": "2026-03-16T10:30:04.000Z"
}
FieldDescription
statusqueued, processing, completed, or failed
outputDataTranslated key-value pairs. Present when status is completed.
errorMessageError description. Present when status is failed.
callbackStatusWebhook delivery state: pending, delivered, or failed.

Polling interval

For most translation jobs, processing takes 2-8 seconds. A 2-second polling interval is a reasonable starting point.

List translation jobs#

text
GET /translation-jobs?engineId=eng_abc123&limit=20&cursor=...

Returns a paginated list of jobs for the specified engine, ordered by creation time (newest first). Use the nextCursor value from the response to fetch subsequent pages.

Webhook delivery#

When a job completes, the platform POSTs the result to your webhook URL.

Webhook URL configuration#

Configure a default webhook URL on your organization in the dashboard. This URL receives callbacks for all translation jobs. Individual jobs can override the default by passing callbackUrl in the request body.

Payload#

json
{
  "type": "translation.completed",
  "jobId": "tjb_A1b2C3d4E5f6G7h8",
  "sourceLocale": "en",
  "targetLocale": "de",
  "data": {
    "lesson_title": "Einführung in maschinelles Lernen",
    "lesson_summary": "Diese Lektion behandelt die Grundlagen..."
  }
}

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

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-signatureHMAC-SHA256 signature of {webhook-id}.{webhook-timestamp}.{body}

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: 30 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours. After 5 failed attempts, the webhook is marked as failed. You can still retrieve the result by polling GET /translation-jobs/:id.

Best practices#

Translate multiple languages in parallel#

Submit one job per target language. Each job processes independently, and webhook callbacks arrive as each language completes. This gives your users the fastest time-to-first-translation while remaining languages continue in the background. Show a progress indicator ("3 of 14 languages ready") that updates as each webhook arrives.

javascript
// Content saved - kick off translations for all target languages
async function onContentSaved(content) {
  const targetLocales = ["de", "fr", "ja", "ko", "pt-BR"];

  const jobs = await Promise.all(
    targetLocales.map((locale) =>
      fetch("https://api.lingo.dev/translation-jobs", {
        method: "POST",
        headers: {
          "X-API-Key": process.env.LINGO_API_KEY,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          sourceLocale: "en",
          targetLocale: locale,
          data: {
            title: content.title,
            summary: content.summary,
            body: content.body,
          },
          idempotencyKey: `${content.id}-${locale}-${content.updatedAt}`,
        }),
      }).then((r) => r.json())
    )
  );

  // All 5 return 202 instantly.
  // Store job IDs to track progress in your UI.
  await db.translationProgress.create({
    contentId: content.id,
    totalLanguages: targetLocales.length,
    completedLanguages: 0,
    jobIds: jobs.map((j) => j.id),
  });
}

Handle webhook callbacks#

Return 200 immediately from your webhook handler, then process the translation asynchronously. This prevents webhook timeouts and ensures reliable delivery. Update your progress tracker so the UI can reflect each completed language in real time.

javascript
app.post("/webhooks/translations", verifyWebhook, async (req, res) => {
  // Return 200 first - process in background
  res.status(200).send("ok");

  const { type, jobId, targetLocale, data } = req.body;

  if (type === "translation.completed") {
    // Save the translated content to your database
    await db.content.update({
      where: { id: extractContentId(jobId) },
      data: { [`content_${targetLocale}`]: data },
    });

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

  if (type === "translation.failed") {
    // Log and alert - the job can be retried or investigated
    console.error(`Translation failed: ${jobId}`, req.body.error);
  }
});

Use idempotency keys#

If your application might submit the same translation twice (e.g., due to retries or duplicate events), pass an idempotencyKey. The platform returns the existing job instead of creating a duplicate. Keys are scoped per engine and expire after 24 hours.

A natural idempotency key: {contentId}-{locale}-{updatedAt}. This ensures a new job is created only when the content actually changes.

Handle partial failures#

Each language is an independent job. If German succeeds but Japanese fails, the German translation is delivered normally. The failed Japanese job appears with status: "failed" and an errorMessage. Retry by submitting a new job with a fresh idempotency key.

Next Steps#

Localization API
Synchronous API reference for real-time translations
LLM Models
Configure per-locale model selection and fallback chains
Glossaries
Enforce exact translations for domain-specific terms
API Keys
Generate and manage API keys for your organization

Was this page helpful?