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

Руководство по интеграции

Публичный REST API и вебхуки для интеграторов: авторизация, эндпоинты, подпись вебхуков, Go SDK и режимы отказа. Примеры кода живые — направьте их на свой хост и ключ в консоли ниже.

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

Обзор

Localization Service предоставляет REST API для программной локализации: отправка контента на перевод, опрос статуса задач, вебхуки, загрузка файлов, CMS-конфиги и статическая локализация изображений (staticloc).

Базовый URL

Публичный хост продукта — https://krakenloc.com. В клиенте указывайте только хост — без суффикса API:

https://krakenloc.com

Все методы API находятся под префиксом:

/internal/api/v1/...

Пример полного URL:

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

Staging и приватные деплои могут использовать другой хост; префикс путей тот же.

Health check (без авторизации)

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

Формат данных

  • Content-Type: application/json (кроме multipart-загрузки файлов)
  • Кодировка: UTF-8
  • Имена полей: camelCase (sourceLang, targetLangs, webhookUrl, taskId, …)
  • Значения enum / протокола: часто snake_case (in_progress, human_review, context_request)
  • Даты: ISO 8601, где применимо

Официальный Go SDK

Модуль: github.com/simple-life-app/localizetion-service/sdk

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

Подробности — в разделе Go SDK.

Аутентификация

Каждый запрос к /internal/api/v1/* требует department API key.

Схема

Authorization: Bearer <api_key>
  • Формат ключа: dk_… (ключ департамента)
  • Middleware проверяет Bearer-токен, привязывает контекст департамента/продукта и применяет rate limit на ключ

Не отправляйте cookie на плоскость внешнего API. Это не admin SSO.

Пример

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"}]
  }'

Типичные ошибки авторизации

Статус Смысл
401 Нет/неверный заголовок Authorization, или формат не Bearer <api_key>
401 Ключ неизвестен, отозван или неактивен
403 Организация приостановлена или ключ не имеет нужного scope

Тело при неверном формате (пример):

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

Учётные данные

Где создавать и хранить секреты для интеграций.

API-ключи департамента

  • Admin UI: страница API Keys продукта — /api-keys
  • Формат ключей: dk_…
  • У каждого ключа может быть свой RateLimitPerMinute
  • Ключ привязан к департаменту (и продукту в multi-product организациях)

Храните ключи в secret manager. Не коммитьте их в репозиторий.

Секреты подписи вебхуков

  • Admin UI: настройки департамента (Settings → Departments → webhook secret департамента)
  • Формат: whsec_… (Standard Webhooks)
  • При ротации предыдущий секрет действует в grace-период, чтобы получатели успели переключиться
  • Права: departments.webhook_secret.read / departments.webhook_secret.manage

Пример секрета (фиктивный, только для документации):

whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw

UI тестирования вебхуков

  • Путь: /developer/webhook-testing
  • Нужны admin SSO и соответствующее право
  • Умеет подписывать тестовые доставки боевым секретом департамента (в теле "test": true)

Эндпоинты

Пути относительно базового хоста. Полный URL = хост + путь ниже.

Метод Путь Описание
GET /_health/live Liveness (без auth)
POST /internal/api/v1/files/upload Загрузка файла; URL/id для items типа file_url
POST /internal/api/v1/cms/configs/upsert Upsert CMS-конфига с внешнего API
POST /internal/api/v1/translate Отправка на перевод
GET /internal/api/v1/tasks/:id Статус / результат задачи
GET /internal/api/v1/tags Список тегов (департамент + global)
GET /internal/api/v1/language-pairs Языковые пары пайплайнов департамента ключа
GET /internal/api/v1/languages Системные языки
POST /internal/api/v1/tasks/:id/context-response Ответ на context_request webhook
POST /internal/api/v1/staticloc/images Загрузка изображения для staticloc
POST /internal/api/v1/staticloc/recognize Запуск recognize
POST /internal/api/v1/staticloc/edit Запуск edit
GET /internal/api/v1/staticloc/tags Теги staticloc
GET /internal/api/v1/staticloc/tasks Список staticloc-задач
GET /internal/api/v1/staticloc/tasks/:id Staticloc-задача

Отдельного URL «result» нет: готовый перевод приходит в GET /internal/api/v1/tasks/:id и/или вебхуком.

Перевод (Translate)

POST /internal/api/v1/translate

Отправка одного или нескольких элементов контента на перевод. Для нескольких языков предпочитайте targetLangs; targetLang по-прежнему принимается для одного языка.

Если задан webhookUrl (или пайплайн долгий), HTTP-ответ часто асинхронный (taskId + status: "queued"). Без вебхука короткие задачи могут вернуть синхронное тело с items / meta (или multi-lang results).

Полное тело запроса (camelCase)

Синтетический пример экрана checkout — форма соответствует живому 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"
      }
    ]
  }
}

Контекст запроса

Поле Назначение
context.message Свободный текст-бриф для лингвиста / пайплайна
context.tags Выбирает пайплайн — имена тегов должны существовать у вашего департамента (GET /internal/api/v1/tags). Неизвестное имя роняет задачу с pipeline_not_found; запрос без тегов матчит только пайплайны без тегов. Правила: Discovery → Tags
context.metadata Непрозрачный набор данных, возвращается в задаче и вебхуке
context.imageUrls / context.fileUrls Справочные материалы для переводчика
additionalInfo.tags Свободные метки для карточки задачи в админке — в подборе пайплайна не участвуют

Типы элементов

type Форма content Заметки
plain string Свободный UI / абзац
keyvalue object → string Ключи — пользовательские данные, wire их не переименовывает
json любой JSON Переводятся строковые листья; числа/булевы/структура сохраняются
file_url string URL Из POST /files/upload

Существующие переводы

  • Порядок existingTranslations[].items должен совпадать с items (число и типы)
  • Языки только в existingTranslations (не в targetLangs) прокидываются as-is
  • Для keyvalue existing может содержать подмножество ключей; остальное переводится
  • forceRetranslate: true игнорирует existing

Асинхронный ответ (очередь)

HTTP 202, когда запрос принят асинхронно (типично при webhookUrl или долгом пайплайне):

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

Дальше: GET /internal/api/v1/tasks/{taskId} или вебхук.

Go SDK: Translate декодирует и HTTP 200, и 202 в *TranslateResponse. На 202 заполняются Status ("queued"), TaskID и Message. Ветвление — через resp.IsQueued() (Status == "queued"):

resp, err := client.Translate(ctx, req)
if err != nil {
    // обработка ошибки
}
if resp.IsQueued() {
    // HTTP 202 — poll GetTask/WaitTask или ждать вебхук
    taskID := resp.TaskID
    _ = taskID
}
// иначе: синхронное тело 200 с items/meta или multi-lang results

Синхронный ответ — один язык

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

Синхронный ответ — multi-lang (results)

В HTTP multi-lang поле называется results (не languageResults — это имя для вебхуков).

{
  "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 у языка: 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

Передайте cmsConfigId (из CMS upsert), чтобы переиспользовать контент CMS и записать переводы обратно:

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

Опрос статуса и результата задачи перевода. В проде предпочтительны вебхуки; polling — для скриптов и отладки.

Отдельного URL /tasks/{id}/result нет — готовый payload в resultData и/или в вебхуке.

Статусы

Status Смысл
new Принята, не стартовала
in_progress Выполняется
human_review Нужно внимание человека
lqa Linguistic QA
ready Успешный терминал (вместе с done)
done Успешно завершена
rejected Отклонена
failed Ошибка
canceled Отменена

Хелперы SDK (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"

Ответ — 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"
}

Ответ — done (с 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"
}

Ответ — 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

task, err := client.GetTask(ctx, taskID)

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

Ответ с контекстом (Context response)

Когда лингвисту или пайплайну нужна дополнительная информация, сервис шлёт вебхук context_request. Ваша система отвечает:

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

Входящий вебхук

Заголовки и подпись — в Webhooks. Тело:

{
  "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 — информационный path. Предпочтительнее authenticated-вызов внешнего API с department API key (ниже).

Тело ответа

{
  "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]"
  }
}
Поле Обяз. Заметки
message да Текст для лингвиста / пайплайна
fileUrls нет Документы
imageUrls нет Скриншоты / мокапы
tags нет Свободные метки ответа — не связаны с context.tags в translate и в подборе пайплайна не участвуют
metadata нет Opaque bag — удобно эхом вернуть contextRequestId

Успешный ответ

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

Поток

  1. Получить подписанный context_request
  2. Собрать контекст (CMS, wiki, дизайн)
  3. POST .../tasks/{taskId}/context-response
  4. Пайплайн продолжается; перевод может перезапуститься с новым контекстом

Files, CMS, Staticloc

Загрузка файла

POST /internal/api/v1/files/uploadmultipart/form-data. Вернувшийся url передайте как file_url item (или сохраните fileId у себя).

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"

Ответ (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"
    }
  ]
}

Далее 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 — создать/обновить CMS-конфиг по project + config slug. configId передайте как cmsConfigId в translate (reuse + write-back).

Запрос:

{
  "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" }
  ]
}

Ответ:

{
  "configId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "added": 2,
  "updated": 1,
  "unchanged": 1,
  "markedNeedsUpdate": 3
}
Поле Смысл
added Новые ключи (targets пустые)
updated Source изменился → targets могут потребовать обновления
unchanged Source совпал
markedNeedsUpdate Пары key×language, помеченные к переводу

SDK: UpsertCMSConfig.

Staticloc (локализация изображений)

Асинхронный API /internal/api/v1/staticloc/*.

Method Path Назначение
POST /internal/api/v1/staticloc/images Upload → imageId
POST /internal/api/v1/staticloc/recognize Recognize job
POST /internal/api/v1/staticloc/edit Edit / localize job
GET /internal/api/v1/staticloc/tasks Список (cursor)
GET /internal/api/v1/staticloc/tasks/:id Задача
GET /internal/api/v1/staticloc/tags Теги профиля департамента

Терминальные статусы: succeeded | failed | canceled.

Create image (201):

{
  "imageId": "img_01JABC2DEF3GH4JK5LM6NP7QRS"
}

Create task (recognize / edit):

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

Create task (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: языки, пары и теги, доступные вашему ключу.

Method Path Scope
GET /internal/api/v1/languages Системные active languages и pairs (не department-scoped)
GET /internal/api/v1/language-pairs Пары пайплайнов департамента ключа
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"

Ответ:

{
  "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 (пайплайны департамента)

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

Ответ:

{
  "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"
    }
  ]
}

По этому списку видно, какие targetLangs реально обрабатываются пайплайнами департамента.

Tags

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

Ответ:

{
  "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
    }
  ]
}

Имена тегов (или id) передаются в context.tags на translate. Теги участвуют в подборе пайплайна, поэтому несуществующий тег не игнорируется — задача с ним падает.

Как теги выбирают пайплайн

Пайплайн подбирается по департаменту, языковой паре, типу контента, уровню качества и тегам. Правило по тегам: все теги, настроенные на пайплайне, должны присутствовать в запросе:

Теги пайплайна context.tags в запросе Совпадение
нет любые или не переданы ✅ пайплайн без тегов — wildcard
["web"] ["web"] или ["web", "promo"] ✅ тег пайплайна присутствует
["web"] не переданы / [] ❌ запрос без тегов попадает только в пайплайны без тегов
["web"] ["cms-content"] ❌ тега пайплайна нет в запросе
["web", "promo"] ["web"] ❌ нужны все теги пайплайна

Имена резолвятся в id тегов до матчинга. Имя, которого нет в списке выше, не резолвится ни во что: пайплайн не находится, задача завершается как failed:

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

Это происходит после ответа 202, поэтому не считайте 202 успехом: опрашивайте GET /internal/api/v1/tasks/{id} или ждите вебхук. Лечится передачей имени тега из этой ручки — либо попросите админа локализации создать тег и привязать его к пайплайну, который должен обрабатывать этот контент.

SDK: ListLanguages, ListLanguagePairs, ListTags.

Webhooks

Передайте webhookUrl в translate (и связанных flow), чтобы получать push при завершении работы или запросе контекста.

Заголовки доставки

Header Описание
webhook-id Уникальный id (ключ идемпотентности между ретраями)
webhook-timestamp Unix seconds (string)
webhook-signature Пробел-разделённые v1,<base64>

Пример:

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=

Проверяйте подпись по сырым байтам body. См. Подписи вебхуков.

Типы

type Когда
translation Задача перевода завершена (deliverable / terminal)
context_request Нужен контекст; ответ через context-response

Только эти два типа на external plane. Нет полей event / task.completed.

Translation webhook — plain (один язык)

Envelope camelCase; nested result — 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
    }
  }
}

Ловушка имён полей

Поверхность Multi-lang map
HTTP POST /translate response results
Webhook nested result languageResults

Ключи внутри keyvalue content никогда не переименовываются.

Поддержите обе формы result

result приходит в одной из двух форм:

Задача Форма
Несколько targetLangs карта по языкам в result.languageResults (ключи — коды языков), плоский result.items присутствует, но равен null
Один целевой язык может прийти в сокращённой форме — элементы этого языка прямо в result.items

Приёмник, написанный только под одну форму, работает до первой задачи другого вида — обычно это проявляется ошибкой вашей же схемы вроде result: expected array, received null, когда в многоязычной доставке нет items.

Читайте result.languageResults, если поле есть, иначе — result.items; структура элементов в обоих случаях одинаковая.

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
}

Ответ: Context response.

Тестовые доставки

"test": true — payload из Webhook Testing (/developer/webhook-testing) с боевым секретом департамента. Подпись проверяйте как в проде; бизнес-эффекты можно фильтровать по test.

Секрет

На департамент: whsec_… в настройках департамента. См. Подписи.

Чеклист приёмника

  1. Быстро вернуть 2xx (обработка async)
  2. Verify signature на raw body
  3. Dedupe по webhook-id
  4. Branch по type
  5. Для translation — nested languageResults

Подписи вебхуков

Исходящие вебхуки следуют спецификации Standard Webhooks (HMAC-SHA256, подписи v1).

Алгоритм

  1. Требуются непустые webhook-id, webhook-timestamp, webhook-signature
  2. Отклонить, если |now − timestamp| > 5 минут (защита от replay)
  3. Секрет должен начинаться с whsec_; HMAC-ключ = Base64-decode части после whsec_
  4. signed_content = id + "." + timestamp + "." + raw_body
  5. expected = Base64(HMAC-SHA256(key, signed_content))
  6. webhook-signaturev1,<base64> через пробел; достаточно любого совпадения

Проверяйте точные сырые байты тела — не сериализуйте JSON заново перед verify.

Go (официальный 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)

Также: ParseWebhook, DecodeTranslationResult.

Node.js (минимум)

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");
  // также проверьте |now - timestamp| <= 300s
}

Python (минимум)

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

Фиктивный пример секрета: whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

Go SDK

Установка

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

Путь модуля: github.com/simple-life-app/localizetion-service/sdk

Быстрый старт

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)
// Второй аргумент — logger.ILogger из commonlib.
var l logger.ILogger // логгер вашего приложения

client, err := sdk.NewClient(
    "https://krakenloc.com", // только хост
    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!"},
    },
})

Методы

Метод 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 /internal/api/v1/staticloc/*
VerifyWebhookSignature / ParseWebhook / DecodeTranslationResult вебхуки

Примеры в модуле

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

Ошибки

Неуспешные HTTP-ответы декодируются в *sdk.APIError с полями error и опциональным message; на 429 может быть задан RetryAfter (секунды).

Ошибки и лимиты

HTTP-коды

Code Типичная причина
200 / 201 / 202 Успех (202 — async staticloc / queued)
400 Битый JSON или параметры
401 Нет/неверный Bearer или формат
403 Suspended org / нет scope
404 Неизвестная задача / ресурс
422 Validation (бизнес-правила)
429 Rate limit
500 Server error

Формы тела ошибки

Простой middleware / rate-limit:

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

Стиль handler'ов (многие external endpoints):

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

Go SDK кладёт failed HTTP body в *sdk.APIError:

Поле Wire / источник Заметки
error JSON error Основная строка ошибки (APIError.Err)
message JSON message Опциональная деталь (APIError.Message)
RetryAfter заголовок Retry-After при HTTP 429 Секунды; не поле JSON-тела (json:"-"); 0, если нет
HTTPStatus HTTP status code Не поле JSON-тела (json:"-")

Error() возвращает error, либо error: message, если message непустой.

Validation (translate)

Дубликат языка в existingTranslations:

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

Rate limits

Лимит на API key (RateLimitPerMinute в admin).

Заголовки:

Header Смысл
X-RateLimit-Limit Лимит в минуту
X-RateLimit-Remaining Остаток окна

При превышении:

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

{"error":"rate limit exceeded"}

Backoff с jitter; не крутите GET /tasks/:id агрессивнее необходимого.

Практические лимиты

  • Вебхуки лучше tight polling
  • Большие keyvalue batch могут резаться server-side
  • Лимит upload — reverse-proxy / конфиг окружения
  • Размер plain-текста — порядка десятков тысяч символов на item; крупные документы — несколько items или file_url

Устранение неполадок

401 — неверный формат auth

  • Нужно: Authorization: Bearer dk_…
  • Отдельное имя заголовка для ключа не поддерживается
  • Лишние пробелы или отсутствие префикса Bearer ломают проверку

401 — неверный ключ

  • Ключ отозван, опечатка или другой environment
  • Проверьте ключ в admin API Keys (/api-keys)

429 — rate limit

  • Сделайте backoff и смотрите X-RateLimit-*
  • При необходимости поднимите RateLimitPerMinute на ключе

Неверный base path

  • Base URL — только хост
  • Пути начинаются с /internal/api/v1/…
  • Health: /_health/live (не под /internal/api/v1)

Ошибки проверки подписи вебхука

  • Проверяйте raw body запроса
  • Секрет должен включать префикс whsec_
  • Учитывайте clock skew (окно 5 минут)
  • Имена заголовков точные: webhook-id, webhook-timestamp, webhook-signature

Приёмник отвечает 400 на многоязычную задачу

Симптом: одноязычные задачи доставляются нормально, а задача с несколькими targetLangs падает на валидации самого приёмника (например, result: expected array, received null), после нескольких ретраев доставка прекращается.

  • В многоязычном теле result.items равен null — отсюда ошибка схемы вида expected array, received null. Все языки лежат в result.languageResults. См. Вебхуки → Поддержите обе формы result.
  • Попытки доставки записываются в задачу (GET /internal/api/v1/tasks/{id}) вместе с HTTP-статусом каждой, так что видно: отказал приёмник, а не сеть.
  • После починки приёмника попросите админа локализации переотправить вебхук по задаче — payload собирается заново из сохранённого результата.

Задача падает сразу после 202 с pipeline_not_found

В errorMessageno pipelines found for any target language: [en->es], иногда с хвостом (unknown tags: [...]).

  • Перечислены неизвестные теги — имён из context.tags нет у вашего департамента. Передавайте имена из GET /internal/api/v1/tags либо попросите создать тег и привязать его к пайплайну. См. Discovery → Tags.
  • Неизвестных тегов нет — теги резолвятся, но ни один пайплайн не покрывает эту комбинацию департамента, языковой пары, типа контента и тегов. Запрос без тегов попадает только в пайплайны без тегов, а пайплайну с тегами нужны все его теги в запросе.
  • Проверьте, что пара вообще поддержана: GET /internal/api/v1/language-pairs показывает, что покрывают пайплайны вашего департамента.
  • Падение терминальное, ретраев нет. Вебхук translation отправляется только для успешных терминальных статусов (ready / done), поэтому упавшую задачу видно через GET /internal/api/v1/tasks/{id}, а не через вебхук.

results vs languageResults

  • HTTP multi-lang translate → results
  • Вложенный payload вебхука translation → languageResults
  • Go SDK: DecodeTranslationResult нормализует structural-ключи

camelCase в ответах

  • Имена полей на wire — camelCase (taskId, sourceLang, …)
  • Значения enum остаются snake для multi-word (in_progress, context_request)
  • В переходном окне сервер может принимать snake_case multi-word ключи в запросах; ответы — только camel