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.
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": truein 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[].itemsmust matchitems(same count / types) - Languages only in
existingTranslations(not intargetLangs) are passed through - For
keyvalue, existing may list a subset of keys; missing keys are translated forceRetranslate: trueignores 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
- Receive signed
context_requestwebhook - Gather context on your side (CMS, wiki, design)
POST .../tasks/{taskId}/context-response- Pipeline continues; translation may re-run with new context
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 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
- Return 2xx quickly (process async if needed)
- Verify signature on raw body
- Dedupe on
webhook-id - Branch on
type - For
translation, decode nested result (languageResults)
Webhook signatures
Outbound webhooks follow the Standard Webhooks specification (HMAC-SHA256, v1 signatures).
Algorithm
- Require non-empty
webhook-id,webhook-timestamp,webhook-signature - Reject if
|now − timestamp| > 5 minutes(replay protection) - Secret must start with
whsec_; HMAC key = Base64-decode of the part afterwhsec_ signed_content = id + "." + timestamp + "." + raw_bodyexpected = Base64(HMAC-SHA256(key, signed_content))webhook-signatureis space-separatedv1,<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
keyvaluebatches 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
Bearerprefix 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
RateLimitPerMinuteon 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.itemsisnull— hence a schema error likeexpected array, received null. Every language lives underresult.languageResults. See Webhooks → Handle bothresultshapes. - 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.tagsdo not exist for your department. Send names fromGET /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-pairslists 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 throughGET /internal/api/v1/tasks/{id}, not through a webhook.
results vs languageResults
- HTTP multi-lang translate →
results - Webhook nested translation payload →
languageResults - Go SDK:
DecodeTranslationResultnormalizes 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