Skip to content

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.completed is emitted
    • scan_terms=true + new terms present: emitted after TB_SCAN finishes, normal detail
    • scan_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.completed is 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 failuretask.failed callback + 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/json

Authentication Headers

See Signing Algorithm. In short:

HeaderValue
X-Loxily-AppKeyProject App Key
X-Loxily-TimestampUnix seconds (valid for 5 minutes)
X-Loxily-SignMD5(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).

FieldTypeRequiredDefaultDescription
client_task_idstringNoIdempotency key to prevent duplicate task creation on network retries. UUID recommended. See Idempotency
namestringYesTask name, 1–200 characters
descriptionstringNo""≤ 2000 characters
target_languagesstring[]YesTarget language codes, 1–50 entries; see Language List for valid codes
xlsx_urlstringEither-orPublicly downloadable XLSX URL (https; ≤ 50MB). System fetches and parses synchronously
stringsobject[]Either-orInline strings array, 1–5000 entries
skip_tmbooleanNofalseWhen true, skip Translation Memory matching
skip_tbbooleanNofalseWhen true, skip Termbase matching
scan_termsbooleanNotrueWhether to run term scanning (TB_SCAN). false → go straight to TRANSLATE and no term_scan.completed callback will fire
auto_apply_termsbooleanNotrueWhether 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_urlstringNoWebhook URL (http or https). No callbacks fire when omitted
callback_eventsstring[]NoAll eventsSubset of event names to subscribe to

Term scan × auto-apply matrix

scan_termsauto_apply_termsBehavior
true (default)true (default)Scan → auto-continue into translation; node events follow immediately after term_scan.completed
truefalseScan → 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

FieldTypeRequiredDescription
string_idstringYesString identifier
contentstringYesSource text
remarkstringNoContext / notes
kb_referencestringNoKnowledge base reference
max_lenstringNoMax 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 existing task_id + response header X-Loxily-Idempotent-Replay: true
js
// ✅ 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)

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

FieldDescription
trace_idTrace ID (top-level). Log it locally so you can look up issues in the Open Platform "Call Logs" page
data.task_idTask ID. Used for subsequent Task Terms Query, status queries, and callback event matching
data.accepted_atServer-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: true

Error codes

codeDescription
400Invalid request / missing signature headers / invalid xlsx_url
401Invalid signature / timestamp outside 5-minute window
403App Secret not configured
404Invalid App Key
413strings count or xlsx_url file too large
422Target language not in project config / no source language configured
500Internal server error
502Workflow 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)

eventWhen it firesKey detail fields
term_scan.completedTerm scan phase finished (either real completion or skipped; not fired when scan_terms=false)skipped / reason / auto_apply_terms / requires_confirmation
task.completedTranslate workflow finished successfully — the main "done" signalpassthrough from the source event
smart_optimize.completedSmart Optimize phase finished (real or skipped-because-no-low-score)real: passthrough; skipped: skipped / reason: 'no_low_score_rows' / threshold
task.failedAny workflow in the pipeline terminated with FAILEDpassthrough error info

detail fields on term_scan.completed:

FieldTypeDescription
skippedbooleantrue = TB_SCAN was not actually executed (all terms already in termbase / no content / prep error)
reasonstringWhen skipped=true: all_exist_in_termbase / no_unique_content / tb_scan_error
auto_apply_termsbooleanEchoes the auto_apply_terms value from create task
requires_confirmationbooleantrue → 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:

eventdescriptionstep_index
tmMatchingTranslation Memory matching1
tbMatchingTermbase matching (against existing termbase)2
kbExtractionKnowledge base extraction3
translateBatchTranslation4
lqaBatchLQA quality check5
translateEnhanceBatchTranslation enhancement6
aiScoringBatchAI scoring7

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>      # metadata

Body:

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

  1. Read 3 headers: X-Loxily-AppKey / X-Loxily-Timestamp / X-Loxily-Sign
  2. Read the raw request body bytes (do NOT parse JSON and re-serialize — bytes will differ)
  3. Look up appSecret by X-Loxily-AppKey
  4. Compute MD5(timestamp + body + appSecret) and compare with X-Loxily-Sign
  5. Optional: verify X-Loxily-Timestamp is 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 dead and visible in the Open Platform "Call Logs" page
  • Each attempt writes live state to call_logs: status: retrying → delivered/dead, attempts increments 1 → 2 → 3 → 4 — integrators can see this live on the Open Platform page
  • Outbound requests are SSRF-guarded. callback_url accepts 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)

bash
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)

bash
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 headers

Node.js — signing + call

javascript
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)

javascript
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.