Webhooks and the live WebSocket tell you about a job the moment it resolves. But neither helps the next morning, after a deploy, or when you want every locale that failed in the last hour. The moment passed; the event is gone. The jobs are not – each one is a durable record on the platform, long after the process that submitted it has moved on.
GET /jobs/localization is how you reach back for those records. It returns your jobs newest first, in pages you walk with a cursor, narrowed by the engine they ran on or the status they ended in. This is the catch-up channel: the durable record you query when you weren't listening live.
GET /jobs/localizationNew to async localization? Start with the Overview. This page assumes you already have jobs to look through. Like every endpoint, it authenticates with your X-API-Key.
Filters and pagination#
GET /jobs/localization?engineId=eng_abc123&status=completed&limit=20&cursor=...| Parameter | Type | Description |
|---|---|---|
engineId | string (optional) | Return only jobs that ran on this localization engine (eng_...). |
status | string (optional) | Return only jobs in this state: queued, processing, completed, completed_with_warnings, or failed. |
limit | number (optional) | Page size. Default 20, maximum 100. |
cursor | string (optional) | Opaque cursor from the previous response's nextCursor. Omit it for the first page. |
Both filters are optional and combine: engineId=eng_abc123&status=failed returns the failed jobs for one engine and nothing else. That combination answers a question you will actually ask in an incident – show me everything that failed on this engine – without pulling back every job in the organization to filter client-side.
The cursor is a position in the result stream, not a page number. You don't compute it; you receive it. Each response hands you a nextCursor, and you pass that value back to fetch the page after it.
Response#
Each page is an items array plus a nextCursor. nextCursor is null on the last page – that is your loop's exit condition, not an error.
{
"items": [
{
"id": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"targetLocale": "ja",
"status": "completed",
"warnings": [],
"createdAt": "2026-03-16T10:30:00.000Z",
"completedAt": "2026-03-16T10:30:06.000Z"
}
],
"nextCursor": "eyJ0IjoiMjAyNi0wMy0xNlQxMDozMDowMC4wMDBaIiwiaSI6ImxqYl9CMmMzRDRlNUY2ZzdIOGk5In0"
}Each item is a summary – enough to locate a job and read its outcome: which locale, which group, what status, when it was created and finished. It deliberately does not carry the translated output. To pull the full outputData and per-stage steps for one of these jobs, take its id and call Get a single job. List to find; fetch to read.
Handle unknown status values gracefully
Match on the status values you know and fall through to a default branch for the rest, rather than crashing the consumer on a value it hasn't seen. Tolerating an unrecognized value is the defensive default for any string enum you don't own – it keeps your reader running instead of throwing on input it can't classify.
Page through every result#
The exit condition is the whole point: keep requesting until nextCursor comes back null. Pass the nextCursor from one response as the cursor of the next, and the loop terminates on its own.
async function listFailedJobs(engineId) {
const failed = [];
let cursor = undefined; // first page: no cursor
do {
const url = new URL("https://api.lingo.dev/jobs/localization");
url.searchParams.set("engineId", engineId);
url.searchParams.set("status", "failed");
url.searchParams.set("limit", "100"); // fewer round-trips
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { "X-API-Key": process.env.LINGO_API_KEY },
});
const { items, nextCursor } = await response.json();
failed.push(...items);
cursor = nextCursor; // null on the last page -> loop ends
} while (cursor);
return failed; // every failed job for this engine
}Raising limit to 100 cuts the number of round-trips for a large backlog; it does not change the result, only how many pages you walk to read it. There is no offset to drift and no page count to keep in sync – the cursor carries your place, and null tells you when you've read everything.
Next steps#
You have the id of a job. The catch-up channel got you here; from here you read the result, or wire up the live channels so next time you hear it as it happens.
