KRAKEN INTELLIGENCEIntegration Manual
DOC NO. KRK-API-V1 · PUBLIC REST API + WEBHOOKS
HOST krakenloc.com · WIRE CAMELCASE · AUTH BEARER DK_…
ISSUED
2026-08
Русский REV · A
Section 0.0 · Introduction

Integration Manual

Public REST API and webhooks for integrators: auth, endpoints, webhook signing, the Go SDK, and failure modes. The code samples are live — point them at your own host and key in the console below.

host krakenloc.com prefix /internal/api/v1 auth Bearer dk_… wire camelCase

Overview

The Localization Service exposes a REST API for programmatic translation: submit content, poll task status, receive webhooks, upload files, manage CMS configs, run static image localization (staticloc), and invoke published AI pre-edit modules.

Base URL

Public product host is https://krakenloc.com. Use the host only as the client base URL — do not append an API path suffix:

https://krakenloc.com

All API methods live under the prefix:

/internal/api/v1/...

Example full URL:

https://krakenloc.com/internal/api/v1/translate

Staging or private deployments may use a different host; keep the same path prefix.

Health check (no auth)

curl -sS "https://krakenloc.com/_health/live"

Wire format

  • Content-Type: application/json (except multipart file upload)
  • Encoding: UTF-8
  • Field names: camelCase (sourceLang, targetLangs, webhookUrl, taskId, …)
  • Enum / protocol values: often snake_case (in_progress, human_review, context_request)
  • Dates: ISO 8601 where present

Official Go SDK

Module: github.com/simple-life-app/localizetion-service/sdk

go get github.com/simple-life-app/localizetion-service/sdk@latest

See Go SDK for install, methods, and examples.

Authentication

Every request to /internal/api/v1/* requires a department API key.

Scheme

Authorization: Bearer <api_key>
  • Key format: dk_… (department API key)
  • Middleware validates the Bearer token, attaches department/product context, and applies per-key rate limits

Do not send cookies for the external API plane. This is not the admin SSO surface.

Example

curl -sS -X POST "https://krakenloc.com/internal/api/v1/translate" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "department": "engineering",
    "sourceLang": "en",
    "targetLang": "ru",
    "items": [{"type": "plain", "content": "Hello"}]
  }'

Common auth errors

Status Meaning
401 Missing/invalid Authorization header, or expected format is not Bearer <api_key>
401 API key unknown, revoked, or inactive
403 Organization suspended or key not allowed for the requested scope

Invalid format response body (illustrative):

{"error": "invalid authorization format, expected: Bearer <api_key>"}

Credentials

Where to create and manage secrets used by integrators.

Department API keys

  • Admin UI: product-scoped API Keys page at /api-keys
  • Keys look like dk_…
  • Each key can have its own RateLimitPerMinute
  • Keys are bound to a department (and product context in multi-product orgs)

Store keys in a secret manager. Never commit them to source control.

Webhook signing secrets

  • Admin UI: department settings (Settings → Departments → department webhook secret)
  • Secret format: whsec_… (Standard Webhooks)
  • Rotation keeps the previous secret valid for a grace window so receivers can switch without downtime
  • Permissions: departments.webhook_secret.read / departments.webhook_secret.manage

Example secret (fake, for docs only):

whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw

Webhook testing UI

  • Path: /developer/webhook-testing
  • Requires admin SSO + permission
  • Can sign test deliveries with a department’s real secret ("test": true in the payload)

Endpoints

All paths are relative to the host base URL. Full path = host + path below.

Method Path Description
GET /_health/live Liveness (no auth)
POST /internal/api/v1/files/upload Upload a file; returns a URL/id for file_url items
POST /internal/api/v1/cms/configs/upsert Upsert a CMS config from the external API
POST /internal/api/v1/translate Submit translation work
GET /internal/api/v1/tasks/:id Get task status / result
GET /internal/api/v1/tasks/:id/segments List a task's translatable segments and their current/initial values
GET /internal/api/v1/tasks/:id/segments/:key/versions Get one segment's full edit-history version chain
GET /internal/api/v1/tags List tags (department + global)
GET /internal/api/v1/language-pairs List language pairs available to the key’s department pipelines
GET /internal/api/v1/languages List system languages
POST /internal/api/v1/tasks/:id/context-response Provide context for a context_request webhook
POST /internal/api/v1/staticloc/text-layout Start a text layout recognition task
GET /internal/api/v1/staticloc/text-layout/tasks/:id Poll a text layout task
POST /internal/api/v1/staticloc/text-erase Start a text erase task (image + optional mask)
GET /internal/api/v1/staticloc/text-erase/tasks/:id Poll a text erase task
GET /internal/api/v1/staticloc/tasks List staticloc tasks
GET /internal/api/v1/staticloc/tasks/:id Get staticloc task
GET /internal/api/v1/ai-modules List AI modules the key may invoke
POST /internal/api/v1/ai-modules/:key/invoke Invoke a published AI module

There is no separate “result” URL: completed translation data is returned on GET /internal/api/v1/tasks/:id and/or delivered via webhook.

Translate

POST /internal/api/v1/translate

Submit one or more content items for translation. Prefer targetLangs for multi-language; targetLang remains accepted for a single language.

When webhookUrl is set (or the pipeline is long-running), the HTTP response is often async (taskId + status: "queued"). Without a webhook, short jobs may return a sync body with items / meta (or multi-lang results).

Full request body (camelCase)

Synthetic example for a mobile checkout screen — shapes match the live DTO.

{
  "department": "Product",
  "sourceLang": "en",
  "targetLangs": ["ru", "de", "fr"],
  "webhookUrl": "https://hooks.acme-games.com/localization/kraken",
  "type": "llm",
  "items": [
    {
      "type": "plain",
      "content": "Complete your purchase"
    },
    {
      "type": "keyvalue",
      "content": {
        "checkout.title": "Checkout",
        "checkout.pay": "Pay now",
        "checkout.secure": "Secure payment",
        "checkout.tax_note": "Tax included where applicable"
      }
    },
    {
      "type": "json",
      "content": {
        "banner": {
          "headline": "Spring Sale",
          "cta": { "label": "Shop now", "target": "/sale" }
        },
        "priority": 1
      }
    },
    {
      "type": "file_url",
      "content": "https://cdn.krakenloc.com/files/doc/a1b2c3d4_release-notes.docx"
    }
  ],
  "existingTranslations": [
    {
      "language": "ru",
      "items": [
        { "type": "plain", "content": "Завершите покупку" },
        {
          "type": "keyvalue",
          "content": {
            "checkout.title": "Оформление заказа",
            "checkout.pay": "Оплатить"
          }
        },
        { "type": "json", "content": {} },
        { "type": "file_url", "content": "" }
      ]
    }
  ],
  "forceRetranslate": false,
  "context": {
    "message": "UI copy for mobile checkout. Keep CTAs short. Do not translate brand name Acme.",
    "tags": ["checkout", "mobile", "release-2.4"],
    "metadata": {
      "ticket": "LOC-1842",
      "appVersion": "2.4.0"
    },
    "imageUrls": [
      "https://cdn.acme-games.com/design/checkout-v24.png"
    ]
  },
  "additionalInfo": {
    "title": "Checkout strings · release 2.4",
    "subtitle": "JIRA LOC-1842",
    "description": "Keyvalue + banner JSON for iOS/Android checkout funnel",
    "externalId": "1842",
    "externalRef": "LOC-1842",
    "icon": "shopping-cart",
    "color": "#FF4D00",
    "deadline": "2026-04-15T18:00:00+03:00",
    "properties": {
      "project": "mobile_app",
      "branch": "release/2.4",
      "priority": "high"
    },
    "tags": [
      { "label": "Production", "value": "prod", "type": "environment", "color": "green" },
      { "label": "P1", "value": "p1", "type": "priority", "color": "red" }
    ],
    "links": [
      {
        "label": "Jira ticket",
        "url": "https://jira.acme-games.com/browse/LOC-1842",
        "type": "jira",
        "primary": true
      },
      {
        "label": "Figma",
        "url": "https://figma.com/file/abc/checkout",
        "type": "external"
      }
    ]
  }
}

Request context

Field Notes
context.message Free-text brief passed to the linguist / pipeline
context.tags Selects the pipeline — tag names must exist for your department (GET /internal/api/v1/tags). An unknown name fails the task with pipeline_not_found; a request without tags only matches pipelines that have no tags. Rules: Discovery → Tags
context.metadata Opaque bag echoed back on the task and the webhook
context.imageUrls / context.fileUrls Reference material for the translator
additionalInfo.tags Free labels for the task card in the admin UI — not used for pipeline matching

Item types

type content shape Notes
plain string Free-form UI / paragraph
keyvalue object → string values Keys are user data — never renamed by the wire layer
json any JSON String leaves translated; numbers/booleans/structure kept
file_url string URL From POST /files/upload

Existing translations

  • Array order of existingTranslations[].items must match items (same count / types)
  • Languages only in existingTranslations (not in targetLangs) are passed through
  • For keyvalue, existing may list a subset of keys; missing keys are translated
  • forceRetranslate: true ignores existing values

Async response (queued)

HTTP 202 when the request is accepted asynchronously (typical when webhookUrl is set, or the job is long-running):

{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "queued",
  "message": "Translation task queued for processing. Use GET /tasks/{id} to check status."
}

Poll GET /internal/api/v1/tasks/{taskId} or wait for the webhook.

Go SDK: Translate decodes both HTTP 200 and 202 into *TranslateResponse. On 202, Status is "queued" and TaskID / Message are set. Use resp.IsQueued() (Status == "queued") to branch:

resp, err := client.Translate(ctx, req)
if err != nil {
    // handle error
}
if resp.IsQueued() {
    // HTTP 202 — poll GetTask/WaitTask or wait for the webhook
    taskID := resp.TaskID
    _ = taskID
}
// else: sync 200 body with items/meta or multi-lang results

Sync response — single language

{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "items": [
    { "type": "plain", "content": "Завершите покупку" },
    {
      "type": "keyvalue",
      "content": {
        "checkout.title": "Оформление заказа",
        "checkout.pay": "Оплатить",
        "checkout.secure": "Безопасная оплата",
        "checkout.tax_note": "Налог включён, где применимо"
      }
    }
  ],
  "meta": {
    "sourceLang": "en",
    "targetLang": "ru",
    "itemsCount": 2,
    "stringsCount": 5,
    "processingTimeMs": 1840,
    "tokensUsed": 312,
    "partialSuccess": false,
    "processingInfo": {
      "pipeline": "product-ui",
      "engine": "openai",
      "model": "gpt-4o",
      "schemaValidated": true
    }
  },
  "metadata": {
    "ticket": "LOC-1842"
  }
}

Sync response — multi-language (results)

HTTP multi-lang uses the field results (not languageResults — that name is for webhooks).

{
  "taskId": "parent-7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "taskIds": {
    "ru": "a1111111-1111-4111-8111-111111111111",
    "de": "a2222222-2222-4222-8222-222222222222",
    "fr": "a3333333-3333-4333-8333-333333333333"
  },
  "results": {
    "ru": {
      "taskId": "a1111111-1111-4111-8111-111111111111",
      "source": "mixed",
      "keySources": {
        "checkout.title": "provided",
        "checkout.pay": "provided",
        "checkout.secure": "translated",
        "checkout.tax_note": "translated"
      },
      "items": [
        { "type": "plain", "content": "Завершите покупку" },
        {
          "type": "keyvalue",
          "content": {
            "checkout.title": "Оформление заказа",
            "checkout.pay": "Оплатить",
            "checkout.secure": "Безопасная оплата",
            "checkout.tax_note": "Налог включён, где применимо"
          }
        }
      ],
      "meta": {
        "sourceLang": "en",
        "targetLang": "ru",
        "itemsCount": 2,
        "stringsCount": 5,
        "processingTimeMs": 1920
      }
    },
    "de": {
      "taskId": "a2222222-2222-4222-8222-222222222222",
      "source": "translated",
      "items": [
        { "type": "plain", "content": "Kauf abschließen" },
        {
          "type": "keyvalue",
          "content": {
            "checkout.title": "Zur Kasse",
            "checkout.pay": "Jetzt bezahlen",
            "checkout.secure": "Sichere Zahlung",
            "checkout.tax_note": "Steuern ggf. inklusive"
          }
        }
      ],
      "meta": {
        "sourceLang": "en",
        "targetLang": "de",
        "itemsCount": 2,
        "stringsCount": 5,
        "processingTimeMs": 2010
      }
    }
  },
  "metadata": {
    "ticket": "LOC-1842"
  }
}

source on a language result: translated | provided | mixed.

curl — plain + webhook

curl -sS -X POST "https://krakenloc.com/internal/api/v1/translate" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "department": "Product",
    "sourceLang": "en",
    "targetLangs": ["ru", "de"],
    "webhookUrl": "https://hooks.acme-games.com/localization/kraken",
    "items": [
      { "type": "plain", "content": "Welcome back, commander." },
      {
        "type": "keyvalue",
        "content": {
          "home.play": "Play",
          "home.settings": "Settings",
          "home.shop": "Shop"
        }
      }
    ],
    "additionalInfo": {
      "title": "Home screen · EN→RU/DE",
      "externalRef": "LOC-1901"
    }
  }'

CMS write-back

Pass cmsConfigId (from CMS upsert) so stored CMS content is reused and results write back into that config:

{
  "department": "CMS - Configs",
  "sourceLang": "en",
  "targetLangs": ["ru", "de"],
  "cmsConfigId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "items": [
    {
      "type": "keyvalue",
      "content": {
        "welcome.title": "Welcome",
        "welcome.body": "Glad you are here"
      }
    }
  ]
}

Tasks

GET /internal/api/v1/tasks/:id

Poll translation task status and result. Prefer webhooks for production; use polling for scripts and debugging.

There is no separate /tasks/{id}/result URL — completed payload lives on this resource (resultData) and/or on the webhook.

Status values

Status Meaning
new Accepted, not started
in_progress Running
human_review Needs human attention
lqa Linguistic QA
ready Successful terminal (with done)
done Completed successfully
rejected Rejected
failed Failed
canceled Canceled

SDK helpers (Go):

  • IsCompleted: ready | done | rejected | failed | canceled
  • IsSuccess: ready | done
  • IsFailed: failed | rejected | canceled

isReadyToApply

Every response carries isReadyToApply: whether the task has progressed far enough for you to apply its translation. Gate your write on this flag rather than on a status list of your own.

The bar is per-department and set by an admin (applyFromStatus on the department): human_review by default — the first status in which a translation exists — or lqa, ready or done when a team wants review, QA or delivery to finish first. A task that ended in rejected, failed or canceled is never ready, whatever the threshold.

The flag is evaluated on every read, so it flips as the task advances — or as an admin changes the department's setting.

contextRequest

The response always carries an explicit contextRequest field (D10): the task's currently active context request, or null when there is none — never omitted. A request belonging to another org/product is also reported as null. See Context response for the full context_request webhook + reply flow this is pulled from.

Field Notes
id Context request id
status e.g. pending
message The question, when set
targetKeys Keys the request is about — [] when none, never omitted
createdAt RFC3339

curl

curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -H "Authorization: Bearer dk_your_department_api_key"

Response — in progress

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "translation",
  "status": "in_progress",
  "isReadyToApply": false,
  "department": "Product",
  "sourceLang": "en",
  "targetLang": "ru",
  "translationType": "llm",
  "engineType": "llm",
  "engineProvider": "openai",
  "engineModel": "gpt-4o",
  "itemsCount": 2,
  "stringsCount": 5,
  "sourceCharactersCount": 86,
  "sourceWordsCount": 14,
  "targetCharactersCount": 0,
  "targetWordsCount": 0,
  "tokensInput": 240,
  "tokensOutput": 0,
  "processingTimeMs": 0,
  "contextRequest": {
    "id": "c0ffee00-1111-4222-8333-444444444444",
    "status": "pending",
    "message": "What does the string \"Season Pass\" refer to?",
    "targetKeys": ["shop.season_pass"],
    "createdAt": "2026-04-10T09:12:05Z"
  },
  "requestData": {
    "items": [
      { "type": "plain", "content": "Complete your purchase" },
      {
        "type": "keyvalue",
        "content": {
          "checkout.title": "Checkout",
          "checkout.pay": "Pay now"
        }
      }
    ]
  },
  "createdAt": "2026-04-10T09:12:01Z",
  "updatedAt": "2026-04-10T09:12:04Z",
  "startedAt": "2026-04-10T09:12:03Z"
}

Response — done (with result)

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "translation",
  "status": "done",
  "isReadyToApply": true,
  "department": "Product",
  "sourceLang": "en",
  "targetLang": "ru",
  "translationType": "llm",
  "engineType": "llm",
  "engineProvider": "openai",
  "engineModel": "gpt-4o",
  "itemsCount": 2,
  "stringsCount": 5,
  "sourceCharactersCount": 86,
  "sourceWordsCount": 14,
  "targetCharactersCount": 92,
  "targetWordsCount": 13,
  "tokensInput": 240,
  "tokensOutput": 118,
  "processingTimeMs": 1840,
  "contextRequest": null,
  "requestData": {
    "department": "Product",
    "sourceLang": "en",
    "targetLangs": ["ru"],
    "items": [
      { "type": "plain", "content": "Complete your purchase" },
      {
        "type": "keyvalue",
        "content": {
          "checkout.title": "Checkout",
          "checkout.pay": "Pay now",
          "checkout.secure": "Secure payment",
          "checkout.tax_note": "Tax included where applicable"
        }
      }
    ]
  },
  "resultData": {
    "items": [
      { "type": "plain", "content": "Завершите покупку" },
      {
        "type": "keyvalue",
        "content": {
          "checkout.title": "Оформление заказа",
          "checkout.pay": "Оплатить",
          "checkout.secure": "Безопасная оплата",
          "checkout.tax_note": "Налог включён, где применимо"
        }
      }
    ],
    "meta": {
      "sourceLang": "en",
      "targetLang": "ru",
      "itemsCount": 2,
      "stringsCount": 5,
      "processingTimeMs": 1840
    }
  },
  "sourceItemMetrics": [
    {
      "index": 0,
      "type": "plain",
      "words": 3,
      "characters": 24,
      "keys": 0
    },
    {
      "index": 1,
      "type": "keyvalue",
      "words": 11,
      "characters": 62,
      "keys": 4,
      "keyMetrics": [
        { "key": "checkout.title", "words": 1, "characters": 8 },
        { "key": "checkout.pay", "words": 2, "characters": 7 },
        { "key": "checkout.secure", "words": 2, "characters": 15 },
        { "key": "checkout.tax_note", "words": 6, "characters": 32 }
      ]
    }
  ],
  "targetItemMetrics": {
    "ru": [
      {
        "index": 0,
        "type": "plain",
        "words": 2,
        "characters": 17,
        "keys": 0
      },
      {
        "index": 1,
        "type": "keyvalue",
        "words": 10,
        "characters": 70,
        "keys": 4,
        "keyMetrics": [
          { "key": "checkout.title", "words": 2, "characters": 18 },
          { "key": "checkout.pay", "words": 1, "characters": 8 },
          { "key": "checkout.secure", "words": 2, "characters": 18 },
          { "key": "checkout.tax_note", "words": 5, "characters": 26 }
        ]
      }
    ]
  },
  "createdAt": "2026-04-10T09:12:01Z",
  "updatedAt": "2026-04-10T09:12:08Z",
  "startedAt": "2026-04-10T09:12:03Z",
  "completedAt": "2026-04-10T09:12:08Z"
}

Response — failed

{
  "id": "8d0f7780-8536-41ef-a55c-f18gd2g01bf8",
  "type": "translation",
  "status": "failed",
  "isReadyToApply": false,
  "department": "Product",
  "sourceLang": "en",
  "targetLang": "ja",
  "errorMessage": "pipeline execution failed: engine timeout after 60s",
  "itemsCount": 1,
  "stringsCount": 1,
  "processingTimeMs": 60120,
  "requestData": {
    "items": [{ "type": "plain", "content": "Season pass" }]
  },
  "createdAt": "2026-04-10T10:00:00Z",
  "updatedAt": "2026-04-10T10:01:02Z",
  "startedAt": "2026-04-10T10:00:01Z",
  "completedAt": "2026-04-10T10:01:02Z"
}

Go SDK

// One-shot poll
task, err := client.GetTask(ctx, taskID)

// Block until terminal status
task, err = client.WaitTask(ctx, taskID, 2*time.Second)
if task.IsSuccess() {
    // read task.ResultData
}

// Write it into your own system only when the department allows it
if task.IsReadyToApply {
    // apply task.ResultData
}

Context response

When a linguist or pipeline needs more information, the service sends a webhook of type context_request. Your system answers with:

POST /internal/api/v1/tasks/:id/context-response

Inbound webhook (what you receive)

See Webhooks for headers and signing. Body shape:

{
  "type": "context_request",
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "contextRequestId": "c0ffee00-1111-4222-8333-444444444444",
  "message": "What does the string \"Season Pass\" refer to — a battle pass or a sports season ticket?",
  "category": "terminology",
  "targetKeys": ["shop.season_pass", "shop.season_pass.desc"],
  "requestedBy": {
    "name": "Anna Petrova",
    "email": "[email protected]"
  },
  "taskInfo": {
    "sourceLang": "en",
    "targetLangs": ["ru", "de"],
    "department": "Product"
  },
  "callbackUrl": "/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/context-response",
  "test": false
}

callbackUrl is informational (path fragment). Prefer the authenticated external API call below with your department API key.

Reply request

{
  "message": "Season Pass is a battle-pass style product in the in-game shop (not sports). Keep the English product name \"Season Pass\" untranslated in RU/DE UI; translate only the description.",
  "fileUrls": [
    "https://cdn.acme-games.com/docs/season-pass-brief.pdf"
  ],
  "imageUrls": [
    "https://cdn.acme-games.com/design/season-pass-store.png"
  ],
  "tags": ["terminology", "brand-voice"],
  "metadata": {
    "contextRequestId": "c0ffee00-1111-4222-8333-444444444444",
    "wiki": "https://wiki.acme-games.com/season-pass",
    "decidedBy": "[email protected]"
  }
}
Field Required Notes
message yes Free-text context for the linguist / pipeline
fileUrls no Supporting documents
imageUrls no Screenshots / mockups
tags no Free labels on the answer — unrelated to context.tags on translate, never used for pipeline matching
metadata no Opaque bag — useful to echo contextRequestId

Success response

{
  "message": "Context response received successfully"
}

curl

curl -sS -X POST \
  "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/context-response" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Season Pass = in-game battle pass. Keep English product name.",
    "imageUrls": ["https://cdn.acme-games.com/design/season-pass-store.png"],
    "metadata": { "contextRequestId": "c0ffee00-1111-4222-8333-444444444444" }
  }'

Go SDK

_, err = client.ProvideContext(ctx, wh.TaskID, &sdk.ProvideContextRequest{
    Message: "Brand voice: friendly, short sentences. Do not translate product names.",
    ImageURLs: []string{
        "https://cdn.acme-games.com/style/screenshot.png",
    },
    Metadata: map[string]any{
        "contextRequestId": wh.ContextRequestID,
        "category":         wh.Category,
    },
    Tags: []string{"brand-voice"},
})

Flow

  1. Receive signed context_request webhook
  2. Gather context on your side (CMS, wiki, design)
  3. POST .../tasks/{taskId}/context-response
  4. Pipeline continues; translation may re-run with new context

Pull alternative (no webhook receiver)

If you cannot run a webhook receiver, poll GET /internal/api/v1/tasks/:id instead: the response's contextRequest field (D10) carries the same active context request — null when there is none — so WaitTask/manual polling can detect a pending question without ever subscribing to webhooks. Reply the same way, via POST .../tasks/{taskId}/context-response. See Tasks for the field shape.

Files, CMS, Staticloc

File upload

POST /internal/api/v1/files/upload — multipart/form-data. Use the returned url as a file_url translation item (or store fileId on your side).

curl -sS -X POST "https://krakenloc.com/internal/api/v1/files/upload" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -F "file=@./release-notes-en.docx"

Response (201):

{
  "files": [
    {
      "fileName": "release-notes-en.docx",
      "fileType": "doc",
      "url": "https://cdn.krakenloc.com/files/doc/9f3c2a1b_release-notes-en.docx",
      "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "size": 48213,
      "fileId": "9f3c2a1b-4d5e-4f60-8a71-b2c3d4e5f607"
    }
  ]
}

Then translate:

{
  "department": "Product",
  "sourceLang": "en",
  "targetLangs": ["ru"],
  "items": [
    {
      "type": "file_url",
      "content": "https://cdn.krakenloc.com/files/doc/9f3c2a1b_release-notes-en.docx"
    }
  ]
}

SDK: UploadFile, UploadFileFromReader.

CMS config upsert

POST /internal/api/v1/cms/configs/upsert — create/update a CMS config addressed by project + config slug. Pass returned configId as cmsConfigId on translate for reuse + write-back.

Request:

{
  "project": "mobile-app",
  "config": "onboarding",
  "sourceLang": "en",
  "targetLangs": ["ru", "de", "fr"],
  "items": [
    { "key": "welcome.title", "value": "Welcome" },
    { "key": "welcome.body", "value": "Glad you are here" },
    { "key": "welcome.cta", "value": "Get started" },
    { "key": "permissions.camera", "value": "Allow camera access" }
  ]
}

Response:

{
  "configId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "added": 2,
  "updated": 1,
  "unchanged": 1,
  "markedNeedsUpdate": 3
}
Field Meaning
added New keys (targets empty)
updated Source changed → targets may need update
unchanged Source identical
markedNeedsUpdate Key×language pairs marked for translation

SDK: UpsertCMSConfig.

Staticloc (image localization)

Async text layout recognition under /internal/api/v1/staticloc/*: send an image, get every text on it with font, color and pixel position, plus the boxes covering it (an erase mask). The model and prompt are configured once per organization by an administrator.

Method Path Purpose
POST /internal/api/v1/staticloc/text-layout Upload image (multipart field image) and start a text layout task
GET /internal/api/v1/staticloc/text-layout/tasks/:id Poll a text layout task
GET /internal/api/v1/staticloc/tasks List tasks (cursor page; textCount instead of the full result)
GET /internal/api/v1/staticloc/tasks/:id Get task

Terminal statuses: succeeded | failed | canceled. An organization without a configuration gets 422 {"error":"text_layout_not_configured"}; a configuration that went stale (engine deactivated, prompt lost its tag, grant revoked) gets 422 {"error":"text_layout_misconfigured"}.

Create task response (202):

{
  "taskId": "8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13",
  "status": "new",
  "pollUrl": "/internal/api/v1/staticloc/text-layout/tasks/8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13"
}

Poll — succeeded:

{
  "taskId": "8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13",
  "type": "text_layout",
  "status": "succeeded",
  "createdAt": "2026-09-30T11:00:00Z",
  "updatedAt": "2026-09-30T11:00:18Z",
  "result": {
    "image": { "width": 1200, "height": 628 },
    "texts": [
      { "index": 0, "text": "SUMMER\nSALE", "x": 94, "y": 74, "fontFamily": "Montserrat",
        "fontWeight": 800, "fontStyle": "normal", "fontSize": 59, "color": "#FFFFFF" }
    ],
    "boxes": [
      { "textIndex": 0, "x": 94, "y": 74, "width": 530, "height": 59 },
      { "textIndex": 0, "x": 94, "y": 139, "width": 422, "height": 59 }
    ],
    "warnings": []
  }
}

List tasks:

{
  "tasks": [ /* TaskResponse with textCount… */ ],
  "nextCursor": "2026-09-30T11:00:00.123Z|8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13"
}

SDK: CreateStaticlocTextLayout, ListStaticlocTasks, GetStaticlocTask, WaitStaticlocTask.

Text erase

Async text removal: send an image (and optionally a mask) and get the same image without its text, in the input's format (PNG → PNG, JPEG → JPEG, WebP → PNG) and size. The model, prompt, quality and input fidelity are configured once per organization by an administrator.

Method Path Purpose
POST /internal/api/v1/staticloc/text-erase Upload image (png/jpeg/webp, ≤10 MB, each side ≤4096 px) and optional mask, start a text erase task
GET /internal/api/v1/staticloc/text-erase/tasks/:id Poll a text erase task

Mask (optional): a PNG of exactly the image size. If any pixel is transparent, transparent pixels mark the region to erase (the OpenAI convention); a fully opaque mask marks it with white pixels. With a mask only that region can change — pixels outside it are copied from the original. Without a mask (maskMode: "none") the model removes the overlaid design text of the whole image.

Errors carry a stable code in error plus a message: 400 invalid_image, image_too_large, mask_unsupported_type, mask_dimensions_mismatch, mask_empty, invalid_mask; 422 text_erase_not_configured, text_erase_misconfigured; 503 queue_unavailable.

Create task response (202):

{
  "taskId": "3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a",
  "status": "new",
  "maskMode": "masked",
  "pollUrl": "/internal/api/v1/staticloc/text-erase/tasks/3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a"
}

Poll — succeeded (url is presigned and valid for expiresInSec seconds; poll again for a fresh one):

{
  "taskId": "3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a",
  "type": "text_erase",
  "status": "succeeded",
  "maskMode": "masked",
  "createdAt": "2026-09-30T11:00:00Z",
  "updatedAt": "2026-09-30T11:00:41Z",
  "result": {
    "imageId": "0e5c7a1d-2b3f-5d6e-9f8a-7b6c5d4e3f2a",
    "url": "https://…",
    "mimeType": "image/png",
    "width": 1200,
    "height": 628,
    "expiresInSec": 900
  }
}

A failed task carries errorType (e.g. content_rejected, mask_empty, edit_rejected, transient_exhausted) and a generic errorMessage. The task list (GET /staticloc/tasks?type=text_erase) shows text erase tasks with their maskMode.

SDK: CreateStaticlocTextErase, GetStaticlocTextEraseTask.

Discovery

Read-only helpers to discover languages, pairs, and tags available to your key.

Method Path Scope
GET /internal/api/v1/languages System-wide active languages and pairs (not department-scoped)
GET /internal/api/v1/language-pairs Pairs used by pipelines of the API key’s department
GET /internal/api/v1/tags Department + global tags

Languages (+ system pairs)

curl -sS "https://krakenloc.com/internal/api/v1/languages" \
  -H "Authorization: Bearer dk_your_department_api_key"

Response:

{
  "languages": [
    {
      "code": "en",
      "name": "English",
      "nativeName": "English",
      "flagEmoji": "🇬🇧",
      "isSource": true,
      "isTarget": true
    },
    {
      "code": "ru",
      "name": "Russian",
      "nativeName": "Русский",
      "flagEmoji": "🇷🇺",
      "isSource": false,
      "isTarget": true
    },
    {
      "code": "de",
      "name": "German",
      "nativeName": "Deutsch",
      "flagEmoji": "🇩🇪",
      "isSource": false,
      "isTarget": true
    },
    {
      "code": "ja",
      "name": "Japanese",
      "nativeName": "日本語",
      "flagEmoji": "🇯🇵",
      "isSource": false,
      "isTarget": true
    }
  ],
  "languagePairs": [
    {
      "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
      "sourceCode": "en",
      "targetCode": "ru",
      "sourceName": "English",
      "targetName": "Russian"
    },
    {
      "id": "4a3615f1-5f9a-42e4-ab1d-1416f93d4412",
      "sourceCode": "en",
      "targetCode": "de",
      "sourceName": "English",
      "targetName": "German"
    }
  ]
}

Language pairs (department pipelines)

curl -sS "https://krakenloc.com/internal/api/v1/language-pairs" \
  -H "Authorization: Bearer dk_your_department_api_key"

Response:

{
  "languagePairs": [
    {
      "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
      "sourceCode": "en",
      "targetCode": "ru",
      "sourceName": "English",
      "targetName": "Russian"
    },
    {
      "id": "5b4726g2-6g0b-43f5-bc2e-2527g04e5523",
      "sourceCode": "en",
      "targetCode": "fr",
      "sourceName": "English",
      "targetName": "French"
    }
  ]
}

Use this list to know which targetLangs your department can actually process via configured pipelines.

Tags

curl -sS "https://krakenloc.com/internal/api/v1/tags" \
  -H "Authorization: Bearer dk_your_department_api_key"

Response:

{
  "tags": [
    {
      "id": "t_01JGLOBAL000000000000001",
      "name": "marketing",
      "description": "Marketing / growth copy",
      "color": "#9B7045",
      "isGlobal": true
    },
    {
      "id": "t_01JDEPT00000000000000002",
      "name": "checkout",
      "description": "Checkout funnel UI",
      "color": "#467A89",
      "isGlobal": false
    },
    {
      "id": "t_01JDEPT00000000000000003",
      "name": "release-2.4",
      "description": "",
      "color": "#5E7F57",
      "isGlobal": false
    }
  ]
}

Pass tag names (or ids) in context.tags on translate. Tags take part in pipeline matching, so a tag that does not exist here is not ignored — it makes the task fail.

How tags select a pipeline

A pipeline is matched on department, language pair, content type, quality level and tags. For tags the rule is every tag configured on the pipeline must be present in the request:

Pipeline tags context.tags on the request Match
none anything, or omitted ✅ untagged pipeline is a wildcard
["web"] ["web"] or ["web", "promo"] ✅ pipeline tag present
["web"] omitted / [] ❌ a request without tags only reaches untagged pipelines
["web"] ["cms-content"] ❌ pipeline tag missing from the request
["web", "promo"] ["web"] ❌ all pipeline tags must be present

Names are resolved to tag ids before matching. A name that is not in the list above resolves to nothing, no pipeline matches, and the task ends as failed:

{
  "status": "failed",
  "errorMessage": "no pipelines found for any target language: [en->es] (unknown tags: [cms-content])"
}

This happens after the 202 response, so poll GET /internal/api/v1/tasks/{id} (or wait for the webhook) instead of treating 202 as success. Fix it by sending a tag name from this endpoint, or ask your localization admin to create the tag and attach it to the pipeline that should handle the content.

SDK: ListLanguages, ListLanguagePairs, ListTags.

Webhooks

Pass webhookUrl on translate (and related flows) to receive push notifications as translations are published, when work completes, or when context is required.

Delivery headers

Every signed delivery includes:

Header Description
webhook-id Unique message id (idempotency key across retries)
webhook-timestamp Unix seconds (string)
webhook-signature Space-separated v1,<base64> signatures

Example:

POST /localization/kraken HTTP/1.1
Host: hooks.acme-games.com
Content-Type: application/json
webhook-id: msg_0d9f2a6c4b7e4c1a9f3e5d8b2a7c6e41
webhook-timestamp: 1744276328
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

Verify against the raw body bytes. See Webhook signatures.

Types

type When
translation A publication portion is ready, or the task reaches its final deliverable status
context_request System needs more context; answer via context-response
task.status_changed Task moves to a new status (incl. creation)
task.tms.assigned Task sent to TMS
task.tms.step_completed A linguist completes a workflow step
task.tms.language_approved A target language is approved
task.tms.language_reopened A target language is reopened
task.tms.withdrawn A TMS assignment is withdrawn
review.completed A human review finishes (task-level or LQA)

There is no separate task.completed / event field — everything is carried in type. The task lifecycle events (task.status_changed, task.tms.*, review.completed) are documented in full, with per-type data shapes, in Task lifecycle events; they are emitted on the same signed channel described on this page and default to ON per department (opt-out list).

Translation webhook — plain (single lang)

Wire envelope is camelCase; nested result uses languageResults.

{
  "type": "translation",
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "done",
  "resultVersion": 1,
  "final": true,
  "department": "Product",
  "sourceLang": "en",
  "targetLang": "ru",
  "targetLangs": ["ru"],
  "completedAt": "2026-04-10T09:12:08Z",
  "test": false,
  "metadata": {
    "ticket": "LOC-1842",
    "clientRequestId": "req-checkout-2.4"
  },
  "publication": {
    "mode": "per_task",
    "portion": 0,
    "languages": {
      "ru": { "state": "complete", "published": 1, "pendingReview": 0, "reviewed": 0 }
    },
    "pendingSegments": [],
    "decisionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?route=review"
  },
  "result": {
    "languageResults": {
      "ru": {
        "items": [
          {
            "type": "plain",
            "content": "Завершите покупку"
          }
        ],
        "keySources": {},
        "source": "translated",
        "hasError": false,
        "error": "",
        "metadata": {
          "sourceLang": "en",
          "targetLang": "ru",
          "itemsCount": 1,
          "stringsCount": 1,
          "processingTimeMs": 1250,
          "processingInfo": {
            "pipeline": "product-ui",
            "engine": "openai",
            "model": "gpt-4o"
          }
        }
      }
    },
    "partialFailure": false,
    "metadata": {}
  }
}

Translation webhook — keyvalue multi-language

{
  "type": "translation",
  "taskId": "a0b1c2d3-e4f5-4678-89ab-cdef01234567",
  "status": "ready",
  "resultVersion": 1,
  "final": true,
  "department": "CMS - Configs",
  "sourceLang": "en",
  "targetLangs": ["de", "es", "fr"],
  "completedAt": "2026-04-10T12:00:00Z",
  "test": false,
  "metadata": {
    "configId": "3398",
    "type": "config",
    "stringIds": "[819793,819794,819795,819796]"
  },
  "publication": {
    "mode": "per_task",
    "portion": 0,
    "languages": {
      "de": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 },
      "es": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 },
      "fr": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 }
    },
    "pendingSegments": [],
    "decisionsUrl": "/internal/api/v1/tasks/a0b1c2d3-e4f5-4678-89ab-cdef01234567/segments?route=review"
  },
  "result": {
    "languageResults": {
      "de": {
        "items": [
          {
            "type": "keyvalue",
            "content": {
              "welcome.title": "Willkommen",
              "welcome.body": "Schön, dass du da bist",
              "welcome.cta": "Loslegen",
              "permissions.camera": "Kamerazugriff erlauben"
            }
          }
        ],
        "keySources": {
          "welcome.title": "translated",
          "welcome.body": "translated",
          "welcome.cta": "translated",
          "permissions.camera": "translated"
        },
        "source": "translated",
        "hasError": false,
        "error": "",
        "metadata": {
          "sourceLang": "en",
          "targetLang": "de",
          "itemsCount": 1,
          "stringsCount": 4,
          "processingTimeMs": 416
        }
      },
      "es": {
        "items": [
          {
            "type": "keyvalue",
            "content": {
              "welcome.title": "Bienvenido",
              "welcome.body": "Nos alegra que estés aquí",
              "welcome.cta": "Empezar",
              "permissions.camera": "Permitir acceso a la cámara"
            }
          }
        ],
        "source": "translated",
        "hasError": false,
        "metadata": {
          "sourceLang": "en",
          "targetLang": "es",
          "processingTimeMs": 421
        }
      },
      "fr": {
        "items": [
          {
            "type": "keyvalue",
            "content": {
              "welcome.title": "Bienvenue",
              "welcome.body": "Ravi de vous voir ici",
              "welcome.cta": "Commencer",
              "permissions.camera": "Autoriser l'accès à la caméra"
            }
          }
        ],
        "source": "translated",
        "hasError": false,
        "metadata": {
          "sourceLang": "en",
          "targetLang": "fr",
          "processingTimeMs": 420
        }
      }
    },
    "partialFailure": false,
    "metadata": {
      "processingInfo": {
        "pipeline": "cms-configs",
        "engine": "openai",
        "model": "gpt-4o"
      },
      "itemsCount": 3,
      "stringsCount": 12,
      "processingTimeMs": 535
    }
  }
}

Publication snapshots

In department settings, Publish results selects when translations are released:

UI option publication.mode Behavior
Whole task per_task (default) Release the task result together; the translation webhook has final: true
Per language per_language Release a language when its segments no longer wait for review
Per segment per_segment Release cleared segments while the remaining segments wait for review

The mode is copied onto a task at creation. Per segment uses the quality profile's Only segments needing review (narrow) or Lock passed (lock) review scope.

Every translation webhook contains top-level resultVersion, final, and publication. Each portion is a full snapshot of everything publishable now, including translations released in earlier portions; it is never a delta.

Field Meaning
resultVersion Task result revision; use this to decide whether to apply the snapshot
final false while the publication cycle continues; true on its last snapshot
publication.mode per_task, per_language, or per_segment
publication.portion Publication sequence number
publication.languages Map keyed by target language; each value has state (pending, partial, or complete), published, pendingReview, and reviewed counts
publication.pendingSegments Authoritative list of cells to keep unpublished or to unpublish
publication.decisionsUrl API path for the task's Review segments; call it with a key that has the tasks scope

reviewed counts published cells cleared by human review. Each pending segment is addressed by lang, zero-based itemIndex, and key. Its reasons array contains layer, check, outcome, and optional severity; an empty array still means the cell must be unpublished. final is top-level, outside publication.

Example: one German cell is released and another remains in Review.

{
  "type": "translation",
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "human_review",
  "department": "Product",
  "sourceLang": "en",
  "targetLang": "de",
  "targetLangs": ["de"],
  "completedAt": "2026-09-30T09:12:08Z",
  "resultVersion": 14,
  "final": false,
  "result": {
    "languageResults": {
      "de": {
        "items": [
          { "type": "keyvalue", "content": { "checkout.title": "Zur Kasse" } }
        ],
        "hasError": false
      }
    }
  },
  "publication": {
    "mode": "per_segment",
    "portion": 3,
    "languages": {
      "de": { "state": "partial", "published": 1, "pendingReview": 1, "reviewed": 0 }
    },
    "pendingSegments": [
      {
        "lang": "de",
        "itemIndex": 0,
        "key": "dose.warning",
        "reasons": [
          { "layer": "l1", "check": "risk.medical_accuracy", "outcome": "veto", "severity": "critical" }
        ]
      }
    ],
    "decisionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?route=review"
  },
  "test": false
}

Delivery is at least once. Dedupe by webhook-id and apply only a resultVersion greater than the last applied version for that task. Delivered versions never decrease, but intermediate versions may be skipped: do not wait for consecutive version or portion numbers. Apply the full snapshot and unpublish every cell listed in pendingSegments, even if an earlier snapshot published it. final: true closes this cycle; a later edit starts a new cycle with a higher resultVersion.

Field name trap

Surface Multi-lang map field
HTTP POST /translate response results
Webhook nested result languageResults

Content keys inside keyvalue maps are never renamed.

Handle both result shapes

result arrives in one of two shapes:

Task Shape
Several targetLangs per-language map under result.languageResults, keyed by language code; the flat result.items is present but null
One target language may use the shortcut shape with that language's items directly under result.items

A receiver built for only one shape works until the first task of the other kind arrives — typically surfacing as a schema error on your side such as result: expected array, received null when a multi-language delivery has no items.

Read result.languageResults when present and fall back to result.items otherwise; both carry the same per-item structure.

Context request webhook

{
  "type": "context_request",
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "contextRequestId": "c0ffee00-1111-4222-8333-444444444444",
  "message": "What does the string \"Season Pass\" refer to — a battle pass or a sports season ticket?",
  "category": "terminology",
  "targetKeys": ["shop.season_pass", "shop.season_pass.desc"],
  "requestedBy": {
    "name": "Anna Petrova",
    "email": "[email protected]"
  },
  "taskInfo": {
    "sourceLang": "en",
    "targetLangs": ["ru", "de"],
    "department": "Product"
  },
  "callbackUrl": "/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/context-response",
  "test": false
}

Reply: Context response.

Test deliveries

When "test": true, the payload was sent from Webhook Testing (/developer/webhook-testing) with a department signing secret. Treat like production for signature verification; ignore for business side-effects if you filter on test.

Secret

Signing secret is per department: whsec_… from department settings. See Webhook signatures.

Receiver checklist

  1. Return 2xx quickly (process async if needed)
  2. Verify signature on raw body
  3. Dedupe on webhook-id
  4. Branch on type
  5. For translation, apply only a greater resultVersion; decode languageResults (or the single-language items shape) as a full snapshot
  6. Unpublish every cell in publication.pendingSegments, including cells published earlier
  7. Treat final: true as the last snapshot of this cycle; accept later edits with higher versions

Webhook signatures

Outbound webhooks follow the Standard Webhooks specification (HMAC-SHA256, v1 signatures).

Algorithm

  1. Require non-empty webhook-id, webhook-timestamp, webhook-signature
  2. Reject if |now − timestamp| > 5 minutes (replay protection)
  3. Secret must start with whsec_; HMAC key = Base64-decode of the part after whsec_
  4. signed_content = id + "." + timestamp + "." + raw_body
  5. expected = Base64(HMAC-SHA256(key, signed_content))
  6. webhook-signature is space-separated v1,<base64> entries; any match accepts

Use the exact raw body bytes that were signed — do not re-serialize JSON before verifying.

Go (official SDK)

err := sdk.VerifyWebhookSignature(
    secret, // whsec_...
    r.Header.Get("webhook-id"),
    r.Header.Get("webhook-timestamp"),
    r.Header.Get("webhook-signature"),
    rawBody,
)
if err != nil {
    // reject
}
typ, payload, err := sdk.ParseWebhook(rawBody)

Also available: ParseWebhook, DecodeTranslationResult.

Node.js (minimal)

const crypto = require("crypto");

function verify(secret, id, timestamp, signatureHeader, rawBody) {
  if (!secret.startsWith("whsec_")) throw new Error("bad secret");
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const signed = `${id}.${timestamp}.${rawBody}`;
  const expected = crypto.createHmac("sha256", key).update(signed).digest("base64");
  const ok = signatureHeader.split(/\s+/).some((part) => {
    const [ver, sig] = part.split(",", 2);
    return ver === "v1" && sig && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  });
  if (!ok) throw new Error("invalid signature");
  // also enforce |now - timestamp| <= 300s
}

Python (minimal)

import base64, hashlib, hmac, time

def verify(secret: str, id: str, timestamp: str, signature_header: str, raw_body: bytes) -> None:
    assert secret.startswith("whsec_")
    key = base64.b64decode(secret[len("whsec_"):])
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("timestamp skew")
    signed = f"{id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for part in signature_header.split():
        ver, _, sig = part.partition(",")
        if ver == "v1" and sig and hmac.compare_digest(expected, sig):
            return
    raise ValueError("invalid signature")

Fake example secret used in docs: whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

Go SDK

Install

go get github.com/simple-life-app/localizetion-service/sdk@latest

Module path: github.com/simple-life-app/localizetion-service/sdk

Quick start

import (
    "context"
    "log"

    "github.com/simple-life-app/commonlib/v2/logger"
    "github.com/simple-life-app/localizetion-service/sdk"
)

// NewClient(baseURL string, l logger.ILogger, opts ...ClientOption)
// Second argument is logger.ILogger from commonlib.
var l logger.ILogger // your application logger

client, err := sdk.NewClient(
    "https://krakenloc.com", // host only
    l,
    sdk.WithAPIKey("dk_your_department_api_key"),
)
if err != nil { log.Fatal(err) }

if err := client.Ping(ctx); err != nil { log.Fatal(err) }

resp, err := client.Translate(ctx, &sdk.TranslateRequest{
    Department: "Product",
    SourceLang: "en",
    TargetLang: "ru",
    Items: []sdk.TranslationItem{
        {Type: sdk.ItemTypePlain, Content: "Hello, world!"},
    },
})

Methods

Method API
Ping GET /_health/live
UploadFile / UploadFileFromReader POST /internal/api/v1/files/upload
Translate POST /internal/api/v1/translate
GetTask / WaitTask GET /internal/api/v1/tasks/{id}
ProvideContext POST /internal/api/v1/tasks/{id}/context-response
GetTaskSegments GET /internal/api/v1/tasks/{id}/segments
GetSegmentVersions GET /internal/api/v1/tasks/{id}/segments/{key}/versions
ListLanguages GET /internal/api/v1/languages
ListLanguagePairs GET /internal/api/v1/language-pairs
ListTags GET /internal/api/v1/tags
UpsertCMSConfig POST /internal/api/v1/cms/configs/upsert
Staticloc helpers /internal/api/v1/staticloc/*
ListAIModules GET /internal/api/v1/ai-modules
InvokeAIModule POST /internal/api/v1/ai-modules/{key}/invoke
VerifyWebhookSignature / ParseWebhook / DecodeTranslationResult webhook helpers

See Segments and edit history for GetTaskSegments / GetSegmentVersions usage and error handling (both return sdk.ErrInvalidArgument for empty required arguments). See AI modules for ListAIModules / InvokeAIModule.

Example files (in the module)

example_translate.go, example_async_poll.go, example_webhook.go, example_files.go, example_cms.go, example_staticloc.go, example_discovery.go, example_additional_info.go, example_existing_translations.go, example_ai_modules.go

Errors

Failed HTTP responses decode into *sdk.APIError with error (or AI-module code) and optional message fields; on 429 and 503, RetryAfter (seconds) may be set.

Errors and limits

HTTP status codes

Code Typical cause
200 / 201 / 202 Success (202 common for async staticloc / queued work)
400 Malformed JSON or bad parameters
401 Missing/invalid Bearer key or wrong format
403 Suspended org or insufficient scope
404 Unknown task or resource
422 Validation error (business rules)
429 Rate limit exceeded
500 Server error

Error body shapes

Simple middleware / rate-limit style:

{
  "error": "rate limit exceeded"
}
{
  "error": "invalid authorization format, expected: Bearer <api_key>"
}

Handler style (many external endpoints):

{
  "error": "invalid_request",
  "message": "Message is required"
}
{
  "error": "internal_error",
  "message": "failed to list languages"
}

Go SDK maps failed HTTP bodies into *sdk.APIError:

Field Wire / source Notes
error JSON error Primary error string (APIError.Err)
message JSON message Optional detail (APIError.Message)
RetryAfter Retry-After header on HTTP 429 Seconds; not a JSON body field (json:"-"); 0 if absent
HTTPStatus HTTP status code Not a JSON body field (json:"-")

Error() returns error, or error: message when message is non-empty.

Validation example (translate)

Duplicate language in existingTranslations:

{
  "error": "existingTranslations: duplicate language: ru"
}

Rate limits

Limits are per API key (RateLimitPerMinute configured on the key in admin).

Response headers on limited endpoints:

Header Meaning
X-RateLimit-Limit Configured per-minute limit
X-RateLimit-Remaining Remaining in the current window

On exceed:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Content-Type: application/json

{"error":"rate limit exceeded"}

Backoff with jitter; do not spin-poll GET /tasks/:id more aggressively than needed.

Practical limits

  • Prefer webhooks over tight polling loops
  • Large keyvalue batches may be split server-side
  • Upload size limits follow reverse-proxy / server configuration for your environment
  • Text size guidance for plain items is on the order of tens of thousands of characters per item — split oversized documents into multiple items or use file_url

Troubleshooting

401 — wrong auth format

  • Must be Authorization: Bearer dk_…
  • A custom header name for the key is not supported
  • Extra whitespace or missing Bearer prefix fails validation

401 — invalid key

  • Key revoked, mistyped, or belongs to another environment
  • Confirm the key in admin API Keys (/api-keys)

429 — rate limit

  • Back off and honor X-RateLimit-* headers
  • Raise RateLimitPerMinute on the key if legitimate traffic needs it

Wrong base path

  • Base URL is host only
  • Paths start with /internal/api/v1/…
  • Health is /_health/live (not under /internal/api/v1)

Webhook signature failures

  • Verify against the raw request body
  • Secret must include the whsec_ prefix
  • Check clock skew (5-minute window)
  • Header names are exact: webhook-id, webhook-timestamp, webhook-signature

Receiver returns 400 on a multi-language task

Symptom: single-language tasks deliver fine, then a task with several targetLangs fails with the receiver's own validation error (e.g. result: expected array, received null), retried a few times and then given up on.

  • In a multi-language body result.items is null — hence a schema error like expected array, received null. Every language lives under result.languageResults. See Webhooks → Handle both result shapes.
  • Attempts are recorded on the task (GET /internal/api/v1/tasks/{id}) with the HTTP status of each try, so you can confirm the receiver — not the network — rejected it.
  • After fixing the receiver, ask your localization admin to re-send the webhook for the affected task; the payload is rebuilt from the stored result.

Task fails right after 202 with pipeline_not_found

errorMessage reads no pipelines found for any target language: [en->es], optionally followed by (unknown tags: [...]).

  • Unknown tag names listed — the names in context.tags do not exist for your department. Send names from GET /internal/api/v1/tags, or have the tag created and attached to the pipeline. See Discovery → Tags.
  • No unknown tags — the tags resolve, but no pipeline covers this combination of department, language pair, content type and tags. A request without tags only matches pipelines that have no tags at all, and a tagged pipeline needs all of its tags present in the request.
  • Check the pair is offered at all: GET /internal/api/v1/language-pairs lists what your department's pipelines cover.
  • The failure is terminal — the task is not retried. Only successful terminal statuses (ready / done) emit a translation webhook, so a failed task is visible through GET /internal/api/v1/tasks/{id}, not through a webhook.

results vs languageResults

  • HTTP multi-lang translate → results
  • Webhook nested translation payload → languageResults
  • Go SDK: DecodeTranslationResult normalizes structural keys

camelCase responses

  • Wire field names are camelCase (taskId, sourceLang, …)
  • Enum values stay snake where multi-word (in_progress, context_request)
  • During a limited migration window the server may still accept snake_case multi-word keys on requests; responses are camel only

Task lifecycle events

Beyond translation and context_request (Webhooks), the service emits a family of task lifecycle events on the same signed channel: same webhookUrl, same Standard Webhooks signature, same attempt log, same retry policy. A task that moves on the kanban, is sent to TMS, has a step completed by a linguist, or is approved after human review now produces a webhook.

Event registry

type Fired when
task.status_changed Every status transition, including creation (from: null)
task.tms.assigned A TMS assignment is created (task sent to TMS)
task.tms.step_completed A linguist completes a workflow step
task.tms.language_approved A target language is approved in the TMS workflow
task.tms.language_reopened A previously approved target language is reopened
task.tms.withdrawn A TMS assignment is withdrawn
review.completed A human review (task-level or LQA) finishes, with the result attached

A failed task does not get a dedicated task.failed type — it surfaces as task.status_changed with data.to: "failed".

Envelope

Every lifecycle event uses the same camelCase envelope as translation/context_request, with an event-specific data object:

{
  "type": "task.status_changed",
  "eventId": "9f8c7b6a-1111-4222-8333-444444444444",
  "occurredAt": "2026-09-09T10:00:00.123456Z",
  "taskId": "550e8400-e29b-41d4-a716-446655440000",
  "department": "marketing",
  "sourceLang": "en",
  "targetLangs": ["de", "fr"],
  "status": "ready",
  "test": false,
  "data": {
    "from": "human_review",
    "to": "ready",
    "changedBy": "550e8400-e29b-41d4-a716-446655440099",
    "note": null
  }
}
Field Notes
eventId UUID, unique per event; use for idempotency/dedupe on top of webhook-id
occurredAt UTC, RFC3339 with nanoseconds
status Task status at delivery time (snake enum)
targetLang / targetLangs Single-lang shortcut / full list, same rule as translation
data Always an object (never omitted); shape depends on type — see below

data by event type

task.status_changed

Field Type Notes
from string | null Previous status; null on task creation
to string New status
changedBy string (uuid) | null User who made the change; null for automatic transitions
note string | null Optional transition note

task.tms.assigned, task.tms.step_completed, task.tms.language_approved, task.tms.language_reopened, task.tms.withdrawn

Field Type Notes
assignmentId string TMS assignment id
targetLang string Language the assignment covers
step string Workflow step
wordCount number Omitted when unknown

Commercial/vendor fields (assignee identity, rates, quotes) are never included on this channel.

review.completed

{
  "type": "review.completed",
  "eventId": "bbbbbbbb-1111-4222-8333-444444444444",
  "occurredAt": "2026-09-09T11:00:00Z",
  "taskId": "550e8400-e29b-41d4-a716-446655440000",
  "department": "marketing",
  "sourceLang": "en",
  "targetLangs": ["ru"],
  "status": "ready",
  "test": false,
  "data": {
    "reviewKind": "task",
    "approvedAt": "2026-09-09T10:59:00Z",
    "approvedBy": "550e8400-e29b-41d4-a716-446655440099",
    "editedSegments": {"ru": 3}
  },
  "result": {
    "languageResults": {
      "ru": {
        "items": [{"type": "plain", "content": "Привет"}],
        "hasError": false
      }
    },
    "partialFailure": false
  },
  "metadata": {}
}
Field Type Notes
data.reviewKind string "task" or "lqa"
data.approvedAt string RFC3339
data.approvedBy string (uuid) Reviewer id
data.editedSegments object Per-language count of segments edited during review
result object | null Same shape as the translation webhook result (languageResults); null when unavailable — never omitted
metadata object Omitted when empty

Manual kanban moves to ready (without going through a review action) emit only task.status_changed, not review.completed.

Subscription — default ALL, opt out per department

By default every department receives every subscribable event type (translation, task.status_changed, task.tms.*, review.completed; context_request is always sent and is not part of the opt-out list). Configure an explicit allow-list in department settings to receive only a subset. If your receiver hard-fails on an unrecognized type, either update it to ignore unknown types gracefully or opt out of the event families you do not handle yet.

Ordering and retries

  • Delivery order across event types is not guaranteed — order by occurredAt if your integration needs a strict sequence.
  • Same retry policy as translation: up to 5 retries (6 attempts total) with backoff; non-2xx and transport errors both retry.
  • task.tms.* events are relayed from an internal outbox on a periodic tick (a few seconds), so they may lag slightly behind the TMS action that produced them.

Receiver checklist (addendum)

  1. Branch on the full type set, not just translation / context_request
  2. For review.completed, handle result: null (task without a stored result yet)
  3. Dedupe on eventId (per-event) in addition to webhook-id (per-delivery)
  4. Treat unrecognized type values as forward-compatible no-ops (ack, don't fail)

Segments and edit history

Read-only access to a task's individual translatable cells ("segments") and their full human-edit history: source text, initial machine output, current (approved-edit-overlaid) value, gate decisions, publication state, best-effort TM/LQA provenance, and every version a segment went through. Both endpoints are under the same tasks scope as GET /internal/api/v1/tasks/:id.

Method Path Description
GET /internal/api/v1/tasks/:id/segments List every segment of a task
GET /internal/api/v1/tasks/:id/segments/:key/versions Get one segment's version chain

Segment identity

A segment is addressed by the triple (lang, itemIndex, key):

  • lang — target language code
  • itemIndex — index of the request item the key belongs to (0-based). Needed because a keyvalue task can send multiple items to the same language, and the same key (e.g. hero.title) can legitimately repeat across items — itemIndex disambiguates them.
  • key — the cell key (for keyvalue/json items) or a synthetic key for plain items

key may contain |, /, %, or Unicode characters and must be URL-escaped as a path segment when calling the versions endpoint directly (the Go SDK does this for you). Every segment's versionsUrl field is already a ready-to-call, escaped link.

List segments

GET /internal/api/v1/tasks/:id/segments

Query param Required Notes
lang no Filter to one target language
route no publish or review (includes reviewed decisions)
publication no published, pending_review, reviewed, or unpublished
layer no Stopping layer: l0, l1, l2, or l3
include no checks to include individual check results
cursor no Opaque cursor from a previous page's nextCursor
limit no Page size; default 200, max 500
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?lang=de&limit=100" \
  -H "Authorization: Bearer dk_your_department_api_key"
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "items": [
    {
      "lang": "de",
      "itemIndex": 0,
      "key": "checkout.title",
      "itemType": "keyvalue",
      "source": "Checkout",
      "initialMt": "Kasse",
      "current": "Zur Kasse",
      "keySource": "content",
      "decision": {
        "route": "review",
        "status": "reviewed",
        "decidedAt": "2026-04-10T10:05:00Z",
        "reasons": [
          { "layer": "", "check": "", "outcome": "human_review", "veto": false }
        ],
        "overridden": false
      },
      "publication": {
        "state": "reviewed",
        "resultVersion": 14,
        "publishedAt": "2026-04-10T10:05:00Z",
        "deliveredAt": "2026-04-10T10:05:01Z",
        "value": "Zur Kasse"
      },
      "versionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments/checkout.title/versions?lang=de&itemIndex=0",
      "tm": {
        "tier": "fuzzy",
        "score": 1,
        "matchPercent": 87,
        "memoryId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "locked": false
      },
      "lqa": [
        {
          "dimension": "accuracy",
          "category": "mistranslation",
          "severity": "minor",
          "status": "open",
          "explanation": "Slightly literal phrasing.",
          "suggestedFix": "Zur Kasse gehen"
        }
      ]
    }
  ],
  "nextCursor": "eyJsYW5nIjoiZGUiLCJpdGVtSW5kZXgiOjAsImtleSI6ImNoZWNrb3V0LnRpdGxlIn0",
  "warnings": []
}
Field Notes
source Source-language text for this cell (empty when the source item could not be matched)
initialMt The cell's value right after the pipeline finished — before any human edit
current Current value with the latest approved edit overlaid (falls back to raw MT output when no approved edit exists)
keySource Where the key came from (e.g. content) when the task's stored key-sources map has it, else empty
tm Best-effort translation-memory match info — omitted when unknown (see below)
provenance Optional pass-through of pipeline node provenance (K-07); omitted when absent
lqa LQA findings for this cell from the newest completed full-LQA assessment for the language (empty array, never omitted)
decision Pipeline gate decision; omitted when none exists (see below)
publication Release state and optional published value/timestamps; always present
versionsUrl Ready-to-call, escaped link to this segment's version chain
warnings See Warnings below

A task without a stored result returns items: []. A lang filter that matches no cells also returns items: [] (not an error).

Filters combine and apply before pagination. Invalid route, publication, layer, or include returns HTTP 400 invalid_filter.

curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?lang=de&route=review&publication=pending_review&layer=l1&include=checks" \
  -H "Authorization: Bearer dk_your_department_api_key"

Gate decisions and publication

Each cell has a publication object. Its decision is the pipeline gate decision, omitted when the pipeline has not produced one for that cell. Manual quality runs are advisory and do not change the route or publish translations.

decision field Meaning
route publish for passed or locked; review for needs_review or reviewed
status passed, needs_review, locked, or reviewed
layer Optional stopping layer: l0 (L0 Format), l1 (L1 Critical risks), l2 (L2 Critical errors), or l3 (L3 Quality score)
profileVersionId Optional UUID of the published quality-profile version used
decidedAt Decision timestamp (ISO 8601)
score Optional segment score
reasons Reason array; present even when empty
checks Individual check results, present only with include=checks; an empty array is valid
overridden Whether a manager overrode the decision

Publish and Review group the gate decisions. route=review includes already reviewed cells; to identify decisions still waiting for action, inspect decision.status == needs_review. A Publish route describes the gate result; publication.state describes whether the cell has been released.

decision.reasons[] field Meaning
layer Layer associated with the reason
check Check identifier
outcome Reason kind, such as fail, veto, critical_finding, below_threshold, check_failed, missing, no_verdict, manual, human_review, or qe
severity Optional neutral, minor, major, or critical
findingId Optional finding UUID
detail Optional explanation
score Optional nonzero score
veto true for reasons at l0, l1, or l2; false otherwise
decision.checks[] field Meaning
check Check identifier
layer Check layer
status ok, failed, not_applicable, or skipped
verdict Optional pass or fail
score Optional check score
publication field Meaning
state published (Published), pending_review (Pending review), reviewed (Reviewed and published), or unpublished
resultVersion Optional first result revision in which this cell was published; different from the task's current revision
publishedAt Optional first-publication timestamp
deliveredAt Successful webhook-delivery timestamp; omitted until acknowledged by a 2xx response
revisedAt Optional timestamp of a revision to the published value
value Optional value from the publication record; import it only when the state is published or reviewed

current includes approved edits and can differ from publication.value. Import only cells whose publication state is published or reviewed. Publication timestamps use ISO 8601. published describes release eligibility and recorded publication; deliveredAt confirms webhook receipt.

TM best-effort note (D3)

tm reflects the task-level processingInfo.tmApplied at the moment the pipeline ran. For a keyvalue task with multiple items, this data uses last-writer semantics — if two items touched the same key, only the last one's TM info survives at the task level, so tm may be missing or belong to a different item than the one you queried. Treat tm as informational, not authoritative. tm is omitted entirely when nothing is known.

Warnings

warnings is a deduplicated array in a fixed order:

Warning Meaning
history_unavailable Edit history could not be loaded; current/initialMt fall back to the raw stored result
lqa_unavailable LQA findings could not be loaded for at least one language; lqa is empty for those cells
publication_unavailable Publication data could not be loaded

Get a segment's version chain

GET /internal/api/v1/tasks/:id/segments/:key/versions

Query param Required Notes
lang yes Target language
itemIndex no Default 0 — see Segment identity above
cursor no Opaque cursor from a previous page's nextCursor
limit no Page size; default 200, max 500. Counts the synthetic mt version on the first page
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments/checkout.title/versions?lang=de&itemIndex=0" \
  -H "Authorization: Bearer dk_your_department_api_key"
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "lang": "de",
  "itemIndex": 0,
  "key": "checkout.title",
  "versions": [
    {
      "n": 1,
      "kind": "mt",
      "value": "Kasse",
      "at": "2026-04-10T09:12:08Z",
      "author": { "kind": "system", "label": "pipeline" },
      "note": null
    },
    {
      "n": 2,
      "kind": "edit",
      "value": "Zur Kasse",
      "at": "2026-04-10T10:02:14Z",
      "author": { "kind": "user", "label": "[email protected]" },
      "note": null
    },
    {
      "n": 3,
      "kind": "approve",
      "value": "Zur Kasse",
      "at": "2026-04-10T10:05:00Z",
      "author": { "kind": "user", "label": "[email protected]" },
      "note": null
    }
  ],
  "nextCursor": "",
  "warnings": []
}

Version kinds

Kind Fired by (internal history action) value author
mt Synthetic — always n: 1, first page only Value right after the pipeline ran {kind: "system", label: "pipeline"}
edit created, value_changed The new value after the edit The editor (or import, see below)
approve review_approved The value before the review action (unchanged by approval) The reviewer
reject review_rejected The value before rejection The reviewer
reset review_reset The value before the reset The reviewer
apply applied The value before it was applied {kind: "system", label: "applied"}
revert reverted, reverted_to_version The restored value (falls back to the edit's original value on legacy rows) The user who reverted

Author kinds

Kind Meaning
user A named human editor or reviewer — label is their email/identifier
agency An agency-affiliated editor — label is always the literal "agency", the real identity is never exposed on this channel
import The edit was created by the bulk-import path (origin:import) — label is the importing user, kind marks it as import-sourced
system An automated action (the initial MT output, or an "applied" step) — no human actor

Paging

Both endpoints use the same opaque-cursor rule: request limit (default 200, max 500), read limit + 1 rows internally, return limit and set nextCursor when more remain. A cursor from one segment/list cannot be reused for a different one — the server returns 400 invalid_cursor if it does not match.

On the versions endpoint specifically: the synthetic mt version is only ever prepended to the first page and counts toward limit — e.g. limit=50 on the first page returns mt plus up to 49 real history entries. Version numbers (n) are stable ordinals that continue across pages (they never restart at n: 1 on a later page).

Errors

Endpoint Status error Cause
List 400 invalid_id Task ID is not a UUID
List 400 invalid_cursor Cursor malformed or from a different coordinate
List 400 invalid_limit limit is not an integer, <= 0, or > 500
List 400 invalid_filter Unknown route, publication, layer, or include value
List 404 task_not_found Unknown task, or task belongs to another org/product
List 500 invalid_task_result Stored task result could not be decoded
List 500 decision_unavailable Pipeline gate decisions could not be loaded
Versions 400 invalid_id | missing_lang | invalid_item_index | invalid_cursor | invalid_limit See above, plus a required lang or a non-integer itemIndex
Versions 404 task_not_found | segment_not_found Unknown/foreign task, or no cell matches (lang, itemIndex, key)
Versions 500 invalid_task_result Stored task result could not be decoded

Go SDK

// List every German segment.
page, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{Lang: "de"})
for _, seg := range page.Items {
    fmt.Println(seg.Key, seg.Current)
}

// Inspect German L1 cells waiting for review, including each check result.
review, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{
    Lang: "de", Route: "review", Publication: "pending_review", Layer: "l1", IncludeChecks: true,
})
_ = review

// Page through the rest with the cursor.
if page.NextCursor != "" {
    next, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{Lang: "de", Cursor: page.NextCursor})
    _ = next
}

// Get one segment's full version chain.
versions, err := client.GetSegmentVersions(ctx, taskID, "de", 0, "checkout.title", "", 0)
for _, v := range versions.Versions {
    fmt.Println(v.N, v.Kind, v.Author.Kind, v.Value)
}

GetTaskSegments/GetSegmentVersions return sdk.ErrInvalidArgument (via errors.Is) when a required argument (task ID, lang, key) is empty — no request is made in that case. key is URL-escaped by the client; limit == 0 omits the query parameter (server default applies).

See Go SDK for the full method table and Context response for contextRequest on the task read.

Prompt & model provenance (K-07)

For every text-LLM pipeline node (mt, ai_agent, lqa) the orchestrator records what actually ran: which prompt version, which model, and what happened. This is historical truth of the execution, not a reconstruction from the current pipeline config — the pipeline can change after a task ran, and provenance still reflects what that task actually used.

Where this data lives

  • Task result — resultData.metadata.processingInfo.provenance on a done (or partially done) task, accumulated across every language and item the task ran (see Tasks).
  • Segments — provenance on each row of GET /internal/api/v1/tasks/:id/segments is a pass-through of the same data, scoped to that segment's (lang, itemIndex) (see Segments); omitted when absent, same as tm.
  • Analytics tables (internal; expand-only columns, additive — nothing existing was renamed or removed) — provider_usage_logs.prompt_version_id and post_edit_analytics.translation_prompt_version_id carry the same promptVersionId, so LLM spend and post-edit quality metrics can be joined back to the exact prompt version that produced a translation.

Shape

{
  "provenance": {
    "nodes": [
      {
        "executionId": "b7c1a9f0-2e34-4b3f-9b3d-2b7f6a9f0c11",
        "targetLang": "de",
        "itemIndex": 0,
        "nodeId": "translate",
        "nodeType": "ai_agent",
        "stepName": "Translate",
        "status": "ok",
        "promptId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "promptVersionId": "c3f1e9a0-6b2d-4e1a-9c3e-7a1b2c3d4e5f",
        "promptVersion": 4,
        "promptName": "Checkout translator",
        "promptKey": "checkout_translate",
        "provider": "openai",
        "model": "gpt-4o",
        "modelSource": "applied",
        "temperature": 0.3,
        "reasoningEffort": null
      }
    ],
    "primary": {
      "executionId": "b7c1a9f0-2e34-4b3f-9b3d-2b7f6a9f0c11",
      "targetLang": "de",
      "itemIndex": 0,
      "nodeId": "translate",
      "nodeType": "ai_agent",
      "status": "ok",
      "promptVersionId": "c3f1e9a0-6b2d-4e1a-9c3e-7a1b2c3d4e5f"
    }
  }
}
Field Notes
nodes One entry per node execution — every mt/ai_agent/lqa node that ran, including nodes that errored or were skipped. Never omitted; [] when nothing ran yet.
nodes[].status ok | error | skipped_no_version (no prompt version was configured to run) | skipped_unavailable (the node's producer wasn't wired for this task)
nodes[].promptId / promptVersionId The exact prompt and version used. promptVersionId is null when the node never resolved one (e.g. skipped_no_version) — it does not fall back to "whatever the prompt's current version is now".
nodes[].model / modelSource See Applied vs configured below
nodes[].itemIndex null for a language-wide record (not per-item); a keyvalue/json task with multiple items sets it per item
primary The node a consumer should treat as the translation step for that (targetLang, itemIndex) — last ai_agent node, or the last mt node when there was no ai_agent step. null when nothing ran.

Applied vs configured

model is what actually produced the response whenever the orchestrator can report it (modelSource: "applied"). Some paths error before a model responds, or a provider doesn't echo back which model served the request — in that case model falls back to what was configured on the node/engine (modelSource: "configured"), which may not be exactly what ran (e.g. a provider-side model alias resolved to a different underlying model). Treat modelSource: "configured" as best-effort, not a hard guarantee.

promptVersionId as a stable join key

The same UUID appears in three places for one translation event:

  1. resultData...provenance.nodes[].promptVersionId (or the segment's pass-through)
  2. provider_usage_logs.prompt_version_id — the LLM call that consumed tokens
  3. post_edit_analytics.translation_prompt_version_id — if that segment was later edited by a human, the row scoring the edit against (targetLang, itemIndex)'s recorded primary node

post_edit_analytics.prompt_version_id (no translation_ prefix) is a different id — the classifier prompt that labeled the edit type, not the translation prompt. Don't confuse the two when joining.

A promptVersionId can go stale: if the prompt or that specific version is later deleted or reverted, historical provenance still shows the id it recorded (this is what actually ran), but looking it up now returns not-found. The admin prompt-chain view (GET /internal/admin/v1/tasks/:id/prompt-chain) surfaces this explicitly as "missing": true on the affected step rather than silently dropping it or resolving to a different version.

Managing prompt versions

Prompt content and versioning are managed through the admin console, not this API — these routes require an authenticated admin session (cookie), not a department API key:

Method Path Permission Purpose
GET /internal/admin/v1/prompts/:id/versions prompts.view List a prompt's versions
POST /internal/admin/v1/prompts/:id/versions prompts.create Push a new version
POST /internal/admin/v1/prompts/:id/versions/:version/revert prompts.edit Roll back to an earlier version
GET /internal/admin/v1/tasks/:id/prompt-chain prompt_proposals.propose The resolved prompt chain for one task, grouped by execution — recorded provenance when available, a reconstruction from the current pipeline otherwise

Pushing a new version does not retroactively change provenance already recorded on past tasks — those keep pointing at the version that actually ran them.

AI modules

Published AI pre-edit modules the calling department API key may invoke. The key needs the ai_modules:invoke scope, the module must be on the key's allowlist, and API exposure must be approved. A module the key may not call is simply absent from discovery.

Method Path Scope
GET /internal/api/v1/ai-modules ai_modules:invoke — list only what this key may invoke
POST /internal/api/v1/ai-modules/{key}/invoke ai_modules:invoke — run the published snapshot

The server never reads surface or run.* from the body: it sets them from the route. Only a published immutable snapshot runs.

SDK: ListAIModules, InvokeAIModule.

Three anchors

The invoke body names the anchor. The server never changes it silently.

Body Anchor Required besides targetLang
taskId set task rowKey — the task row
cmsConfigId set cms rowKey — the CMS row
neither text text — the source string
both taskId and cmsConfigId — 400 ambiguous_anchor

A missing task or CMS config and one that belongs to another tenant answer the same 404 (task_not_found / cms_config_not_found).

Discovery

curl -sS "https://krakenloc.com/internal/api/v1/ai-modules" \
  -H "Authorization: Bearer dk_your_department_api_key"

Response:

{
  "items": [
    {
      "key": "shorten",
      "name": "Shorten",
      "version": 3,
      "interfaceVersion": 2,
      "maxVariants": 3,
      "acceptsInline": ["description", "maxLength"],
      "params": [
        {
          "name": "level",
          "type": "enum",
          "exposure": "cell_toolbar",
          "default": "b1",
          "options": [{"value": "b1", "label": "B1"}]
        }
      ],
      "modes": [
        {
          "mode": "api_task",
          "anchor": "task",
          "state": "full",
          "losses": [],
          "requires": []
        },
        {
          "mode": "api_text",
          "anchor": "text",
          "state": "requires",
          "losses": [{"source": "tm", "reason": "no_anchor", "impact": "skipped"}],
          "requires": ["description", "maxLength"]
        }
      ]
    }
  ]
}

items is [] (HTTP 200) when the API surface is off for the organization, the key has no exposed published modules, or the caller is unidentified. Discovery never lists a module that is pending approval, disabled, unpublished, or closed to the key.

Each modes[] entry is one API mode the exposure opens (api_task, api_cms, api_text). state is full, degraded, unavailable, or requires. losses name sources that do not reach the prompt; requires / acceptsInline name inline fields the plan can take. Parameter names are the module author's and travel verbatim. params lists only the parameters a call may set (exposure is cell_toolbar or run_dialog); parameters the author fixed are not listed.

Invoke — task + row

curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "rowKey": "checkout.cta",
    "sourceLang": "en",
    "targetLang": "ru-RU",
    "currentText": "Перейти к оформлению",
    "maxVariants": 2
  }'

Invoke — CMS config + row

curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "cmsConfigId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
    "rowKey": "checkout.cta",
    "sourceLang": "en",
    "targetLang": "ru-RU"
  }'

Invoke — plain text

targetLang is required on every call. text is required on the text anchor. Optional context is untrusted inline input (arrays, never maps, for row-keyed data).

curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Proceed to checkout",
    "currentText": "Перейти к оформлению заказа",
    "sourceLang": "en",
    "targetLang": "ru-RU",
    "params": {"level": "b1"},
    "maxVariants": 2,
    "expectInterfaceVersion": 2,
    "strictContext": false,
    "context": {
      "description": "Primary checkout button",
      "maxLength": 24,
      "uiType": "button",
      "styleNotes": "Keep the verb.",
      "metadata": {"screen": "checkout"}
    }
  }'

Response (result fields only — source contents and trace are never returned):

{
  "invocationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "module": {"key": "shorten", "version": 3, "interfaceVersion": 2},
  "status": "changed",
  "text": "Оформить заказ",
  "requestId": "fcb18b18-a335-4d8b-8fc4-42e85f211e49",
  "anchor": "text",
  "quality": "normal",
  "edits": [
    {
      "from": "Перейти к оформлению заказа",
      "to": "Оформить заказ",
      "category": "length"
    }
  ],
  "alternatives": ["К оформлению"],
  "diff": [
    {"op": "delete", "text": "Перейти к "},
    {"op": "equal", "text": "оформлению"},
    {"op": "insert", "text": "Оформить заказ"}
  ],
  "usedSources": [{"id": "key_metadata", "origin": "inline"}],
  "degraded": [{"source": "tm", "reason": "no_anchor", "retryable": false}],
  "usage": {
    "model": "gpt-4.1-mini",
    "inputTokens": 420,
    "outputTokens": 48,
    "latencyMs": 890
  }
}

usedSources and degraded carry identifiers and reasons only. notes and edits[].reason are omitted unless the module's API exposure turns explanations on (explain=false by default). invocationId is the journal row; null is not used on this public path.

status is changed, no_change, or cannot_comply. quality is normal or low_context. diff is a word-level diff of the current value and text; whitespace belongs to the piece.

Missing data: degraded and insufficient_context

A call can still succeed when optional sources do not arrive. Those sources appear in degraded (retryable says whether a later retry might fill them). Discovery already advertises the same loss on the mode (state: degraded).

A hard quality floor fails before the model is called and is not billed:

{
  "code": "insufficient_context",
  "message": "not enough context to run the module",
  "details": {
    "missing": ["tm"],
    "unlockWith": ["taskId", "rowKey"],
    "retryable": true
  }
}

strictContext: true turns the soft floor into this 422.

A required source with no inline fallback is 422 required_source_unavailable (details.source, details.reason, and details.missing = the plan's requires fields). A module with no plan for the named anchor is 422 mode_not_supported (details.supportedAnchors).

Body limits

Limit Cap
Request body 1 MiB (over → 413 invalid_request)
context.neighbors 50
context.siblings 10
context.glossaryHints 50
context.styleNotes 500 characters
context.metadata 1500 characters (keys + values)
context.description 1000 characters
context.uiType 64 characters
String value in params 2000 characters (over → 400 invalid_params)
maxVariants 1–5
context.screenshotUrl absolute https URL only

A context field over its cap is 400 invalid_request.

params are checked against the parameter schema of the version the call runs — the latest published snapshot, or the one pinned with moduleVersion — before any source is read or the model is called. A parameter the call leaves out takes the module's default. A parameter whose default is null is required: send a non-empty value (the custom module requires params.custom), otherwise the call answers 400 invalid_params with reason required. Any mismatch is 400 invalid_params, and details.issues lists every problem, sorted by parameter name (at most 20):

reason When
unknown The module declares no parameter with this name
fixed The author fixed the parameter; a call may not set it
null The value is null
wrong_type The value does not match the type: enum and string take a JSON string, int a whole number, bool true or false
not_an_option The enum value is not one of its options[].value
too_long The string is longer than 2000 characters. Before counting, control characters (line breaks included), bidirectional embeddings, overrides and isolates, and Unicode tag characters are removed
required The parameter has no default, and the call does not send it or sends a string without a single visible character: empty, or only spaces, control characters or invisible formatting characters (such as zero-width spaces)

For example, "params": {"level": "z9", "tone": "casual"} against the module in the discovery example answers:

{
  "code": "invalid_params",
  "message": "parameter \"level\": the value is not one of its options (and 1 more)",
  "details": {
    "issues": [
      {"param": "level", "reason": "not_an_option"},
      {"param": "tone", "reason": "unknown"}
    ]
  }
}

message never repeats the values you sent.

targetLang, and sourceLang when sent, must be known language codes. Region, script, and variant subtags are fine (pt-BR, es-419, zh-Hant-TW, de-CH-1996); a code with an extension or a private-use part (en-US-u-ca-gregory, de-t-en, en-x-test) is 400 invalid_request.

expectInterfaceVersion mismatch is 409 module_interface_changed. Pin an older snapshot with moduleVersion (must be ≥ 1).

Error codes

Error bodies are { "code": "…", "message": "…", "details": {…} }. Frequency refusals also set Retry-After (seconds).

HTTP code When
400 invalid_request Body not parsed, missing rowKey/text/targetLang, unknown language code or one with an extension, context field over its cap, bad UUID, blocked screenshot URL
400 ambiguous_anchor Both taskId and cmsConfigId
400 invalid_params params do not match the parameter schema of the version the call runs: unknown name, fixed parameter, null, wrong type, value outside the options, string over 2000 characters, required parameter missing (details.issues)
401 — Missing or invalid Bearer key (middleware)
403 module_not_exposed Module not open to this key
403 engine_not_granted Engine not granted to the organization
403 budget_exceeded Daily USD budget exhausted (fail-closed)
404 module_not_found Unknown module key
404 task_not_found Unknown or foreign task
404 cms_config_not_found Unknown or foreign CMS config
409 module_interface_changed expectInterfaceVersion did not match
413 invalid_request Body larger than 1 MiB
422 mode_not_supported No plan for this anchor (details.supportedAnchors)
422 required_source_unavailable Required source missing (details.source, details.reason)
422 insufficient_context Hard quality floor; not billed
429 rate_limited Key frequency limit, module×key limit, or in-flight cap; Retry-After
500 internal_error Unexpected failure
502 llm_failed Model call failed
503 modules_disabled API surface off for the organization (invoke only; discovery returns [])
503 limiter_unavailable Frequency check could not run (fail-closed); Retry-After
504 llm_timeout Model call timed out

Checks run in order: exposure → key frequency → module×key limit → daily budget → anchor → quality floor. Every 422 is answered before the model is called. A limiter that cannot answer refuses the call (503) instead of letting it through.

Go SDK

listed, err := client.ListAIModules(ctx)
for _, m := range listed.Items {
    fmt.Println(m.Key, m.Version, m.MaxVariants)
}

result, err := client.InvokeAIModule(ctx, "shorten", &sdk.AIModuleInvokeRequest{
    Text:       "Proceed to checkout",
    SourceLang: "en",
    TargetLang: "ru-RU",
})
fmt.Println(result.Text, result.Status, result.Anchor)

Failed module responses decode into *sdk.APIError: Err is the wire code, Message is message. On 429 and 503, RetryAfter is taken from Retry-After.