Руководство по интеграции
Публичный REST API и вебхуки для интеграторов: авторизация, эндпоинты, подпись вебхуков, Go SDK и режимы отказа. Примеры кода живые — направьте их на свой хост и ключ в консоли ниже.
Обзор
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 - Для
keyvalueexisting может содержать подмножество ключей; остальное переводится 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"},
})
Поток
- Получить подписанный
context_request - Собрать контекст (CMS, wiki, дизайн)
POST .../tasks/{taskId}/context-response- Пайплайн продолжается; перевод может перезапуститься с новым контекстом
Files, CMS, Staticloc
Загрузка файла
POST /internal/api/v1/files/upload — multipart/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_… в настройках департамента. См. Подписи.
Чеклист приёмника
- Быстро вернуть 2xx (обработка async)
- Verify signature на raw body
- Dedupe по
webhook-id - Branch по
type - Для
translation— nestedlanguageResults
Подписи вебхуков
Исходящие вебхуки следуют спецификации Standard Webhooks (HMAC-SHA256, подписи v1).
Алгоритм
- Требуются непустые
webhook-id,webhook-timestamp,webhook-signature - Отклонить, если
|now − timestamp| > 5 минут(защита от replay) - Секрет должен начинаться с
whsec_; HMAC-ключ = Base64-decode части послеwhsec_ signed_content = id + "." + timestamp + "." + raw_bodyexpected = Base64(HMAC-SHA256(key, signed_content))webhook-signature—v1,<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
- Большие
keyvaluebatch могут резаться 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
В errorMessage — no 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