Create a localization job group: one request that fans your content out to every target locale you name.
You have a payload of strings and a list of locales, and you want to translate them all without writing the fan-out yourself. POST /jobs/localization takes the whole payload and up to 100 target locales in a single request, then returns 202 Accepted right away with a group ID and one job per locale. One request, every locale – the platform creates the jobs and processes each one independently.
POST /jobs/localizationThis page covers the create call: its parameters, the request shape, the 202 response, and how to make the call safe to retry. New to async localization? Start with the Async Localization API overview for the mental model. Once a group exists, tracking a job group tells you what each locale's status means.
Authentication
Pass your API key in the X-API-Key header. Keys are organization-scoped and reach every engine in the organization. See Authentication for details.
Parameters#
sourceLocale, targetLocales, and data are required. Everything else tunes behavior or makes the call safer to repeat.
| Parameter | Type | Description |
|---|---|---|
sourceLocale | string | BCP-47 source locale (e.g. en). |
targetLocales | string[] | BCP-47 target locales (e.g. ["de", "fr", "ja"]). 1–100 per request. One job is created per locale. |
data | object | Key-value content to translate. Nested objects and arrays are allowed at any depth. |
context | string (optional) | Broad context for this translation payload, such as the product surface, audience, or purpose. Applies to every job created for the request. |
hints | object (optional) | Per-key context as arrays of breadcrumb strings, to disambiguate short or reused strings. |
callbackUrl | string (optional) | HTTPS webhook URL for this group. Overrides the organization default. HTTP is rejected. |
idempotencyKey | string (optional) | Client-generated key. Send the same request twice with the same key and the existing group is returned instead of a new one. Scoped per engine. |
engineId | string (optional) | Localization engine to run the jobs through. Falls back to the organization's default engine when omitted. |
pipelineConfig | object (optional) | Per-request pipeline overrides. Stages you omit inherit from the engine config. |
lockedKeys | string[] (optional) | Keys or glob patterns whose values are excluded from translation and merged back verbatim into outputData. Up to 100 patterns. See Lock non-translatable keys. |
Request#
The data field accepts flat key-value pairs or nested structures with objects and arrays at any depth. The engine translates every string value, preserves non-string values (numbers, booleans, null) untouched, and returns the exact shape you sent. So you can hand it the same object your app already stores – no flattening, no reshaping.
{
"sourceLocale": "en",
"targetLocales": ["de", "fr", "ja"],
"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": "course_101-v3"
}HTTPS required
The callbackUrl must use HTTPS. HTTP URLs are rejected with a 400 error.
That nested payload mixes translatable text with values that must survive untouched – id, course_101, difficulty. Strings are translated; the rest is preserved by type. When you need a string held back too (a slug, an asset URL, an enum code), name it in lockedKeys and it is merged back verbatim into every locale's output.
Response (202 Accepted)#
The call returns immediately. It does not wait for translation – it hands you the group ID and the per-locale job IDs, then the platform processes each job independently in the background.
{
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"status": "pending",
"jobs": [
{ "id": "ljb_A1b2C3d4E5f6G7h8", "targetLocale": "de", "status": "queued" },
{ "id": "ljb_B2c3D4e5F6g7H8i9", "targetLocale": "fr", "status": "queued" },
{ "id": "ljb_C3d4E5f6G7h8I9j0", "targetLocale": "ja", "status": "queued" }
],
"createdAt": "2026-03-16T10:30:00.000Z"
}| Field | Description |
|---|---|
groupId | ljg_-prefixed identifier for the whole group. Store this – it is the handle for tracking and live progress. |
status | Group status at creation, normally pending. |
jobs | One entry per target locale: id (ljb_-prefixed), targetLocale, and the job's status. |
createdAt | ISO 8601 timestamp. |
Three locales in, three jobs back, each queued and ready to run. What each status means as the jobs progress – and what happens when one locale fails while the others ship – lives on Track a job group.
Examples#
The same request from Node and Python. Both fire one POST and read the group ID and job count straight off the 202.
const response = await fetch("https://api.lingo.dev/jobs/localization", {
method: "POST",
headers: {
"X-API-Key": process.env.LINGO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceLocale: "en",
targetLocales: ["de", "fr", "ja"],
data: {
title: "Introduction to Machine Learning",
steps: [
{ heading: "What is ML?", body: "Machine learning is a subset of AI." },
{ heading: "Supervised Learning", body: "Training with labeled data." },
],
},
callbackUrl: "https://your-app.com/webhooks/translations",
}),
});
const { groupId, jobs } = await response.json();
// 202 Accepted – the call returns without waiting for translation.
console.log(groupId); // "ljg_A1b2C3d4E5f6G7h8"
console.log(jobs.length); // 3 – one queued job per target localeMake the call safe to retry#
The natural place to fire this request is a save hook or an event handler – exactly the code that runs twice when a retry fires or a duplicate event arrives. Without protection, two calls mean two job groups, and the same content is queued for translation twice.
Pass an idempotencyKey and that stops being a risk. Send the same request twice with the same key and the platform returns the existing group instead of creating a new one – no second set of jobs. Keys are scoped per engine, so the same key against a different engine is a different group.
Pick a key that means something
A good key combines content identity with version: {contentId}-v{contentVersion}. The same content at the same version always resolves to the same group, so a retry is automatically a no-op. Bump the version when the content changes and you get a fresh group.
const key = `${content.id}-v${content.version}`;
async function submit() {
const response = await fetch("https://api.lingo.dev/jobs/localization", {
method: "POST",
headers: {
"X-API-Key": process.env.LINGO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceLocale: "en",
targetLocales: ["de", "fr", "ja", "ko", "pt-BR"],
data: { title: content.title, steps: content.steps },
callbackUrl: "https://your-app.com/webhooks/translations",
idempotencyKey: key,
}),
});
return (await response.json()).groupId;
}
const first = await submit();
const again = await submit(); // same key – duplicate submission
console.log(first === again); // true – same group returned, no second set of jobsThis is the one POST that fans a payload out to every locale, and it is safe to fire from the same code path that retries. Store the groupId; that is what you carry into tracking and live progress.
