Create Translation Task
Create a translation task via API with specified target languages and source content. This endpoint is asynchronous: the call returns immediately with a task_id and trace_id; subsequent progress is delivered to your callback_url at each workflow node.
Equivalent to the manual flow
This endpoint is equivalent to the frontend "Tasks → New Task + Upload File + default-enabled Term Scan + Auto Apply" flow. The source language is taken from the project's configuration (not accepted as a parameter).
Behavior: Fast return + background execution
The endpoint returns immediately with task_id and trace_id (~100ms). All heavy work runs in the background afterwards: parsing input → building CSV → uploading to GCS → triggering workflows. Any background failure fires a task.failed callback (provided you supplied callback_url).
Why this design?
GCS upload and GCP Workflow invocation can be slow (especially fetching a 50MB xlsx_url). Moving those to the background keeps the API response deterministic at ~100ms.
Task Pipeline (3 workflows chained automatically)
The background flow drives up to 3 workflows in sequence:
Sync return (task_id, accepted_at)
│
▼ (background)
① Term Scan (TB_SCAN) ← only actually executed when scan_terms=true (default) and new terms exist
│ term_scan.completed (also fired for skipped paths with detail.skipped=true)
▼
② Translate (TRANSLATE) ← TM / termbase / KB / translate / LQA / enhance / scoring
│ task.completed
▼
③ Smart Optimize (TRANSLATE_QA) ← only actually executed when low-score rows need a second pass
│ smart_optimize.completed (also fired for skipped paths with detail.skipped=true)- When
term_scan.completedis emittedscan_terms=true+ new terms present: emitted after TB_SCAN finishes, normaldetailscan_terms=true+ all terms already in termbase: emitted immediately,detail.skipped=true, reason='all_exist_in_termbase'scan_terms=true+ prep-stage error: emitted immediately,detail.skipped=true, reason='tb_scan_error'scan_terms=false: not emitted (user explicitly disabled scanning)
- When
smart_optimize.completedis emitted- Low-score rows exist: emitted after TRANSLATE_QA finishes
- No low-score rows: emitted right after
task.completed,detail.skipped=true, reason='no_low_score_rows'
- Any background step failure →
task.failedcallback + full error visible in the "Call Logs" page - After term scan completes, fetch the discovered terms via the Task Terms Query endpoint
Endpoint
POST {BASE_URL}/api/open/v1/tasks/create
Content-Type: application/jsonAuthentication Headers
See Signing Algorithm. In short:
| Header | Value |
|---|---|
X-Loxily-AppKey | Project App Key |
X-Loxily-Timestamp | Unix seconds (valid for 5 minutes) |
X-Loxily-Sign | MD5(timestamp + body + appSecret) |
Request Body
The body is pristine business JSON — no authentication fields mixed in.
Project is resolved automatically
No need to send project_id — the server resolves the project from X-Loxily-AppKey (App Key and project have a 1-to-1 mapping).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
client_task_id | string | No | — | Idempotency key to prevent duplicate task creation on network retries. UUID recommended. See Idempotency |
name | string | Yes | — | Task name, 1–200 characters |
description | string | No | "" | ≤ 2000 characters |
target_languages | string[] | Yes | — | Target language codes, 1–50 entries; see Language List for valid codes |
xlsx_url | string | Either-or | — | Publicly downloadable XLSX URL (https; ≤ 50MB). System fetches and parses synchronously |
strings | object[] | Either-or | — | Inline strings array, 1–5000 entries |
skip_tm | boolean | No | false | When true, skip Translation Memory matching |
skip_tb | boolean | No | false | When true, skip Termbase matching |
scan_terms | boolean | No | true | Whether to run term scanning (TB_SCAN). false → go straight to TRANSLATE and no term_scan.completed callback will fire |
auto_apply_terms | boolean | No | true | Whether to auto-continue into TRANSLATE after the term scan completes. false → task waits in waiting_confirmation until /tasks/confirm-terms is called (only meaningful when scan_terms=true) |
callback_url | string | No | — | Webhook URL (http or https). No callbacks fire when omitted |
callback_events | string[] | No | All events | Subset of event names to subscribe to |
Term scan × auto-apply matrix
scan_terms | auto_apply_terms | Behavior |
|---|---|---|
true (default) | true (default) | Scan → auto-continue into translation; node events follow immediately after term_scan.completed |
true | false | Scan → wait for API confirmation. Use /tasks/terms to list, optional /tasks/terms/update / /tasks/terms/delete to edit, then /tasks/confirm-terms to kick off translation |
false | (ignored) | Skip scanning, translate directly; no term_scan.completed callback |
When scan_terms=true, the term_scan.completed callback detail includes auto_apply_terms and requires_confirmation. If requires_confirmation=true, you must call /tasks/confirm-terms to proceed.
strings item schema
| Field | Type | Required | Description |
|---|---|---|---|
string_id | string | Yes | String identifier |
content | string | Yes | Source text |
remark | string | No | Context / notes |
kb_reference | string | No | Knowledge base reference |
max_len | string | No | Max length constraint for the translation |
Idempotency
The problem: Client requests often fail to receive a response due to network timeouts or connection resets — you don't know whether the server actually created the task. Automatic retries then create duplicate tasks (translated twice, charged twice).
The solution: Generate a client_task_id (UUID recommended) once, and reuse it across the entire retry loop. The server will:
- See
(project_id, client_task_id)for the first time → create the task normally, return HTTP 200 - See the same
(project_id, client_task_id)again within 24 hours → skip creation, return the existingtask_id+ response headerX-Loxily-Idempotent-Replay: true
// ✅ Correct — one client_task_id shared across retries
const clientTaskId = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
try {
return await fetch(url, { body: JSON.stringify({ ..., client_task_id: clientTaskId }) });
} catch { continue; }
}
// ❌ Wrong — new UUID per attempt defeats idempotency
for (let i = 0; i < 3; i++) {
const clientTaskId = crypto.randomUUID(); // different each loop → duplicate tasks
...
}Omitting client_task_id is allowed — the endpoint works fine but won't deduplicate; every call creates a new task. Suitable for one-shot scripts or when you already guarantee non-duplicate invocations upstream. Strongly recommended for production integrations.
Response
Success (HTTP 200)
{
"success": true,
"code": 0,
"msg": "ok",
"trace_id": "8a3f12ab-4d2a-9f51-7e2c6d8a9101",
"data": {
"task_id": "68a12cb7e1f9a88b4ea23c77",
"accepted_at": "2026-04-22T10:22:31.123Z"
}
}Response field reference:
| Field | Description |
|---|---|
trace_id | Trace ID (top-level). Log it locally so you can look up issues in the Open Platform "Call Logs" page |
data.task_id | Task ID. Used for subsequent Task Terms Query, status queries, and callback event matching |
data.accepted_at | Server-accept timestamp |
Why only these two fields?
All heavy operations (CSV building, GCS upload, workflow invocation) run in the background. When the endpoint returns, the background hasn't yet decided whether the first step is term scan or direct translate — so pipeline_stage isn't returned either. Integrators learn the progression through callback events or query endpoints.
trace_id is a top-level common response field — see Common Parameters · Common Response Fields.
Idempotent replay returns HTTP 200 with an additional header:
X-Loxily-Idempotent-Replay: trueError codes
| code | Description |
|---|---|
| 400 | Invalid request / missing signature headers / invalid xlsx_url |
| 401 | Invalid signature / timestamp outside 5-minute window |
| 403 | App Secret not configured |
| 404 | Invalid App Key |
| 413 | strings count or xlsx_url file too large |
| 422 | Target language not in project config / no source language configured |
| 500 | Internal server error |
| 502 | Workflow trigger failed |
Callback Mechanism
If callback_url is supplied, the system POSTs to it at each workflow node via HTTP(S) (both http and https accepted).
Event list
Callbacks are split into two categories: workflow-level (marks phase milestones in the pipeline) and TRANSLATE sub-step (fine-grained progress inside the translate workflow).
Workflow-level events (4)
| event | When it fires | Key detail fields |
|---|---|---|
term_scan.completed | Term scan phase finished (either real completion or skipped; not fired when scan_terms=false) | skipped / reason / auto_apply_terms / requires_confirmation |
task.completed | Translate workflow finished successfully — the main "done" signal | passthrough from the source event |
smart_optimize.completed | Smart Optimize phase finished (real or skipped-because-no-low-score) | real: passthrough; skipped: skipped / reason: 'no_low_score_rows' / threshold |
task.failed | Any workflow in the pipeline terminated with FAILED | passthrough error info |
detail fields on term_scan.completed:
| Field | Type | Description |
|---|---|---|
skipped | boolean | true = TB_SCAN was not actually executed (all terms already in termbase / no content / prep error) |
reason | string | When skipped=true: all_exist_in_termbase / no_unique_content / tb_scan_error |
auto_apply_terms | boolean | Echoes the auto_apply_terms value from create task |
requires_confirmation | boolean | true → integrator must call /tasks/confirm-terms to continue translation |
TRANSLATE sub-step events (7)
The translate workflow runs 7 internal sub-steps in sequence. Each emits a terminal event:
| event | description | step_index |
|---|---|---|
tmMatching | Translation Memory matching | 1 |
tbMatching | Termbase matching (against existing termbase) | 2 |
kbExtraction | Knowledge base extraction | 3 |
translateBatch | Translation | 4 |
lqaBatch | LQA quality check | 5 |
translateEnhanceBatch | Translation enhancement | 6 |
aiScoringBatch | AI scoring | 7 |
11 events total. Omitting callback_events subscribes to all; providing it acts as a whitelist.
Subscription strategy
- Basic integration: subscribe to the 4 workflow-level events
term_scan.completed/task.completed/smart_optimize.completed/task.failed— they cover the pipeline - Real-time progress bar: add the 7 TRANSLATE sub-step events
Callback signing (same scheme as inbound API)
Callbacks use the same signing scheme as the inbound API: all authentication via headers, body stays pristine business JSON. The signature reuses the project's appSecret — no separate Webhook Secret.
Callback request
POST {callback_url}
Content-Type: application/json
X-Loxily-AppKey: <your App Key>
X-Loxily-Timestamp: <unix seconds>
X-Loxily-Sign: MD5(timestamp + body + appSecret)
X-Loxily-Event: translateBatch # metadata, not in signature
X-Loxily-Trace-Id: <trace_id from the create call> # metadata
X-Loxily-Delivery-Id: <unique UUID per delivery> # metadataBody:
{
"event": "translateBatch",
"task_id": "68a12cb7e1f9a88b4ea23c77",
"project_id": "proj-7f3e12ab",
"trace_id": "8a3f12ab-4d2a-9f51-7e2c6d8a9101",
"delivery_id": "a1b2c3...",
"timestamp": "2026-04-22T10:25:17.455Z",
"status": "SUCCEEDED",
"step_index": 4,
"total_steps": 7,
"detail": { "rows_total": 512, "rows_done": 512 },
"error": null
}status values: RUNNING / SUCCEEDED / COMPLETED / FAILED.
Verification steps
- Read 3 headers:
X-Loxily-AppKey/X-Loxily-Timestamp/X-Loxily-Sign - Read the raw request body bytes (do NOT parse JSON and re-serialize — bytes will differ)
- Look up
appSecretbyX-Loxily-AppKey - Compute
MD5(timestamp + body + appSecret)and compare withX-Loxily-Sign - Optional: verify
X-Loxily-Timestampis within an acceptable window (e.g. 5 minutes) for replay protection
Retry policy
- Success condition: HTTP 2xx, responded within 10 seconds
- Up to 4 attempts (1 initial + 3 retries) with backoff:
16s → 32s → 64s - Total retry window ~112 seconds; after exhaustion the delivery is marked
deadand visible in the Open Platform "Call Logs" page - Each attempt writes live state to
call_logs:status: retrying → delivered/dead,attemptsincrements 1 → 2 → 3 → 4 — integrators can see this live on the Open Platform page - Outbound requests are SSRF-guarded.
callback_urlaccepts both http and https, and RFC1918 private ranges (common for VPN-reachable integrator services), but cloud metadata addresses (169.254.0.0/16) and loopback (127.0.0.0/8) are always blocked
112-second window
The current version uses inline synchronous retries with a total window of ~112 seconds. Keep your webhook receiver's single-incident recovery time under 2 minutes, otherwise that callback will be permanently lost (but call_logs will contain a dead event for auditing).
Examples
cURL (inline strings)
APP_KEY="5685414646a54423c891d87194d87f3f"
APP_SECRET="<your App Secret>"
BASE_URL="https://api.loxily.com"
TIMESTAMP=$(date +%s)
BODY='{"client_task_id":"c2a9f5e4-4bb2-4bf3-9fd7-ce30b3a9321c","name":"Q2 onboarding copy","target_languages":["en","ja","ko"],"strings":[{"string_id":"home.title","content":"欢迎来到 Loxily","remark":"home hero","max_len":"32"},{"string_id":"home.cta","content":"立即开始","kb_reference":"brand.voice"}],"callback_url":"https://your-service.example.com/loxily/webhook"}'
SIGN=$(printf '%s' "${TIMESTAMP}${BODY}${APP_SECRET}" | md5)
curl -X POST "${BASE_URL}/api/open/v1/tasks/create" \
-H "Content-Type: application/json" \
-H "X-Loxily-AppKey: ${APP_KEY}" \
-H "X-Loxily-Timestamp: ${TIMESTAMP}" \
-H "X-Loxily-Sign: ${SIGN}" \
-d "${BODY}"cURL (xlsx_url)
BODY='{"client_task_id":"b9d6e5c2-1f8e-4c24-a3c2-2a5b8f12b98a","name":"Q4 campaign","target_languages":["en-us","ja"],"xlsx_url":"https://cdn.example.com/loxily-uploads/2026-04/activity-q4.xlsx","callback_url":"https://your-service.example.com/loxily/webhook"}'
# Send with the same TIMESTAMP + SIGN headersNode.js — signing + call
import crypto from 'crypto';
const body = JSON.stringify({
client_task_id: crypto.randomUUID(),
name: 'Demo task',
target_languages: ['en', 'ja'],
strings: [{ string_id: 'home.title', content: '欢迎来到 Loxily' }],
callback_url: 'https://your-service.example.com/loxily/webhook',
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const sign = crypto.createHash('md5')
.update(`${timestamp}${body}${process.env.LOXILY_APP_SECRET}`, 'utf8')
.digest('hex');
const resp = await fetch(`${process.env.LOXILY_BASE_URL}/api/open/v1/tasks/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Loxily-AppKey': process.env.LOXILY_APP_KEY,
'X-Loxily-Timestamp': timestamp,
'X-Loxily-Sign': sign,
},
body, // IMPORTANT: use the same body string, do NOT JSON.stringify again
});
const data = await resp.json();
console.log(data.trace_id, data.data?.task_id);Webhook verifier (Node.js / Express)
import crypto from 'crypto';
import express from 'express';
// Use express.raw to preserve the exact bytes used to compute the signature.
const app = express();
app.post(
'/loxily/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const timestamp = req.header('X-Loxily-Timestamp') || '';
const sign = req.header('X-Loxily-Sign') || '';
const appKey = req.header('X-Loxily-AppKey') || '';
// Replay protection — 5 minute window
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).json({ ok: false });
}
// Look up appSecret by appKey (shared with inbound API)
const appSecret = process.env.LOXILY_APP_SECRET;
const rawBody = req.body.toString('utf8');
const expected = crypto.createHash('md5')
.update(`${timestamp}${rawBody}${appSecret}`, 'utf8')
.digest('hex');
if (expected !== sign) return res.status(401).json({ ok: false });
const payload = JSON.parse(rawBody);
// TODO: handle the event (e.g. update local progress based on event + status)
console.log(payload.event, payload.status, payload.task_id);
res.json({ ok: true });
},
);Why express.raw?
The signature is computed over the raw request bytes. Using express.json() parses the JSON first, so you only get the object — attempting to JSON.stringify(req.body) back will produce a subtly different byte sequence (whitespace, number precision, key ordering may differ), causing the signature check to fail.
Tracing
The trace_id in the response can be searched in the Open Platform page's "Call Logs" tab to view the full history: whether the request was accepted, every callback event delivery attempt, and final status.