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, and run static image localization (staticloc).

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/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/images Upload image for static localization
POST /internal/api/v1/staticloc/recognize Start recognize job
POST /internal/api/v1/staticloc/edit Start edit job
GET /internal/api/v1/staticloc/tags List staticloc tags
GET /internal/api/v1/staticloc/tasks List staticloc tasks
GET /internal/api/v1/staticloc/tasks/:id Get staticloc task

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

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",
  "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,
  "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",
  "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,
  "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",
  "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
}

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

Files, CMS, Staticloc

File upload

POST /internal/api/v1/files/uploadmultipart/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 image localization under /internal/api/v1/staticloc/*.

Method Path Purpose
POST /internal/api/v1/staticloc/images Upload image → imageId
POST /internal/api/v1/staticloc/recognize Start recognize job
POST /internal/api/v1/staticloc/edit Start edit / localize job
GET /internal/api/v1/staticloc/tasks List tasks (cursor page)
GET /internal/api/v1/staticloc/tasks/:id Get task
GET /internal/api/v1/staticloc/tags Profile tags for the department

Terminal statuses: succeeded | failed | canceled.

Create image response (201):

{
  "imageId": "img_01JABC2DEF3GH4JK5LM6NP7QRS"
}

Create task request (recognize / edit):

{
  "imageId": "img_01JABC2DEF3GH4JK5LM6NP7QRS",
  "tags": ["ui-mock", "store"],
  "meta": {
    "screen": "shop.season_pass",
    "localeHint": "ru"
  }
}

Create task response (202):

{
  "taskId": "st_01JXYZ9ABC0DEF1GH2JK3LM4NO",
  "status": "queued"
}

Get task — succeeded:

{
  "id": "st_01JXYZ9ABC0DEF1GH2JK3LM4NO",
  "type": "edit",
  "status": "succeeded",
  "resultText": "Сезонный пропуск",
  "finishReason": "stop",
  "resultImage": {
    "imageId": "img_01JRESULT00000000000000001",
    "url": "https://cdn.krakenloc.com/staticloc/presigned/…?X-Amz-Expires=900"
  },
  "meta": {
    "screen": "shop.season_pass",
    "localeHint": "ru"
  },
  "promptId": "prm_01J…",
  "engineId": "eng_01J…",
  "createdAt": "2026-04-10T11:00:00Z",
  "updatedAt": "2026-04-10T11:00:42Z"
}

List tasks:

{
  "tasks": [ /* TaskResponse… */ ],
  "nextCursor": "eyJpZCI6InN0XzAxSi4uLiJ9"
}

Tags:

{
  "tags": [
    { "id": "tag_ui", "name": "ui-mock" },
    { "id": "tag_store", "name": "store" }
  ]
}

SDK: UploadStaticlocImage, StaticlocRecognize, StaticlocEdit, ListStaticlocTasks, GetStaticlocTask, WaitStaticlocTask, ListStaticlocTags.

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 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 Translation task finished (deliverable / terminal as configured)
context_request System needs more context; answer via context-response

Only these two types are emitted on the external plane. There is no task.completed / event field.

Translation webhook — plain (single lang)

Wire envelope is camelCase; nested result uses languageResults.

{
  "type": "translation",
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "done",
  "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"
  },
  "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",
  "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]"
  },
  "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
    }
  }
}

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, decode nested result (languageResults)

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
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/*
VerifyWebhookSignature / ParseWebhook / DecodeTranslationResult webhook helpers

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

Errors

Failed HTTP responses decode into *sdk.APIError with error and optional message fields; on 429, 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