Руководство по интеграции
Публичный REST API и вебхуки для интеграторов: авторизация, эндпоинты, подпись вебхуков, Go SDK и режимы отказа. Примеры кода живые — направьте их на свой хост и ключ в консоли ниже.
Обзор
Localization Service предоставляет REST API для программной локализации: отправка контента на перевод, опрос статуса задач, вебхуки, загрузка файлов, CMS-конфиги, статическая локализация изображений (staticloc) и вызов опубликованных AI-модулей прередактуры.
Базовый 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/tasks/:id/segments |
Список переводимых сегментов задачи с текущими/изначальными значениями |
GET |
/internal/api/v1/tasks/:id/segments/:key/versions |
Полная цепочка версий истории правок одного сегмента |
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/text-layout |
Запуск распознавания разметки текста |
GET |
/internal/api/v1/staticloc/text-layout/tasks/:id |
Опрос задачи разметки текста |
POST |
/internal/api/v1/staticloc/text-erase |
Запуск удаления текста (изображение + необязательная маска) |
GET |
/internal/api/v1/staticloc/text-erase/tasks/:id |
Опрос задачи удаления текста |
GET |
/internal/api/v1/staticloc/tasks |
Список staticloc-задач |
GET |
/internal/api/v1/staticloc/tasks/:id |
Staticloc-задача |
GET |
/internal/api/v1/ai-modules |
AI-модули, которые ключ может вызвать |
POST |
/internal/api/v1/ai-modules/:key/invoke |
Вызов опубликованного AI-модуля |
Отдельного 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
isReadyToApply
В каждом ответе есть isReadyToApply: дошла ли задача до состояния, с которого перевод можно
применять у себя. Проверяйте этот флаг, а не собственный список статусов.
Порог задаёт админ на уровне департамента (applyFromStatus): по умолчанию human_review —
первый статус, в котором перевод уже существует, — либо lqa, ready, done, если команде
нужно сначала завершить приёмку, QA или доставку. Задача, завершившаяся в rejected, failed
или canceled, не готова ни при каком пороге.
Флаг вычисляется на каждое чтение: он меняется и при продвижении задачи, и при смене настройки департамента.
contextRequest
Ответ всегда содержит явное поле contextRequest (D10): текущий активный запрос
контекста задачи, либо null, если такого нет — поле никогда не опускается. Запрос,
принадлежащий другому org/product, тоже отображается как null. Полный флоу вебхука
context_request + ответа на него — в разделе Ответ с контекстом.
| Поле | Заметки |
|---|---|
id |
ID запроса контекста |
status |
например pending |
message |
Вопрос, если задан |
targetKeys |
Ключи, к которым относится запрос — [], если нет; никогда не omitted |
createdAt |
RFC3339 |
curl
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer dk_your_department_api_key"
Ответ — in progress
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "translation",
"status": "in_progress",
"isReadyToApply": false,
"department": "Product",
"sourceLang": "en",
"targetLang": "ru",
"translationType": "llm",
"engineType": "llm",
"engineProvider": "openai",
"engineModel": "gpt-4o",
"itemsCount": 2,
"stringsCount": 5,
"sourceCharactersCount": 86,
"sourceWordsCount": 14,
"targetCharactersCount": 0,
"targetWordsCount": 0,
"tokensInput": 240,
"tokensOutput": 0,
"processingTimeMs": 0,
"contextRequest": {
"id": "c0ffee00-1111-4222-8333-444444444444",
"status": "pending",
"message": "What does the string \"Season Pass\" refer to?",
"targetKeys": ["shop.season_pass"],
"createdAt": "2026-04-10T09:12:05Z"
},
"requestData": {
"items": [
{ "type": "plain", "content": "Complete your purchase" },
{
"type": "keyvalue",
"content": {
"checkout.title": "Checkout",
"checkout.pay": "Pay now"
}
}
]
},
"createdAt": "2026-04-10T09:12:01Z",
"updatedAt": "2026-04-10T09:12:04Z",
"startedAt": "2026-04-10T09:12:03Z"
}
Ответ — done (с result)
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "translation",
"status": "done",
"isReadyToApply": true,
"department": "Product",
"sourceLang": "en",
"targetLang": "ru",
"translationType": "llm",
"engineType": "llm",
"engineProvider": "openai",
"engineModel": "gpt-4o",
"itemsCount": 2,
"stringsCount": 5,
"sourceCharactersCount": 86,
"sourceWordsCount": 14,
"targetCharactersCount": 92,
"targetWordsCount": 13,
"tokensInput": 240,
"tokensOutput": 118,
"processingTimeMs": 1840,
"contextRequest": null,
"requestData": {
"department": "Product",
"sourceLang": "en",
"targetLangs": ["ru"],
"items": [
{ "type": "plain", "content": "Complete your purchase" },
{
"type": "keyvalue",
"content": {
"checkout.title": "Checkout",
"checkout.pay": "Pay now",
"checkout.secure": "Secure payment",
"checkout.tax_note": "Tax included where applicable"
}
}
]
},
"resultData": {
"items": [
{ "type": "plain", "content": "Завершите покупку" },
{
"type": "keyvalue",
"content": {
"checkout.title": "Оформление заказа",
"checkout.pay": "Оплатить",
"checkout.secure": "Безопасная оплата",
"checkout.tax_note": "Налог включён, где применимо"
}
}
],
"meta": {
"sourceLang": "en",
"targetLang": "ru",
"itemsCount": 2,
"stringsCount": 5,
"processingTimeMs": 1840
}
},
"sourceItemMetrics": [
{
"index": 0,
"type": "plain",
"words": 3,
"characters": 24,
"keys": 0
},
{
"index": 1,
"type": "keyvalue",
"words": 11,
"characters": 62,
"keys": 4,
"keyMetrics": [
{ "key": "checkout.title", "words": 1, "characters": 8 },
{ "key": "checkout.pay", "words": 2, "characters": 7 },
{ "key": "checkout.secure", "words": 2, "characters": 15 },
{ "key": "checkout.tax_note", "words": 6, "characters": 32 }
]
}
],
"targetItemMetrics": {
"ru": [
{
"index": 0,
"type": "plain",
"words": 2,
"characters": 17,
"keys": 0
},
{
"index": 1,
"type": "keyvalue",
"words": 10,
"characters": 70,
"keys": 4,
"keyMetrics": [
{ "key": "checkout.title", "words": 2, "characters": 18 },
{ "key": "checkout.pay", "words": 1, "characters": 8 },
{ "key": "checkout.secure", "words": 2, "characters": 18 },
{ "key": "checkout.tax_note", "words": 5, "characters": 26 }
]
}
]
},
"createdAt": "2026-04-10T09:12:01Z",
"updatedAt": "2026-04-10T09:12:08Z",
"startedAt": "2026-04-10T09:12:03Z",
"completedAt": "2026-04-10T09:12:08Z"
}
Ответ — failed
{
"id": "8d0f7780-8536-41ef-a55c-f18gd2g01bf8",
"type": "translation",
"status": "failed",
"isReadyToApply": false,
"department": "Product",
"sourceLang": "en",
"targetLang": "ja",
"errorMessage": "pipeline execution failed: engine timeout after 60s",
"itemsCount": 1,
"stringsCount": 1,
"processingTimeMs": 60120,
"requestData": {
"items": [{ "type": "plain", "content": "Season pass" }]
},
"createdAt": "2026-04-10T10:00:00Z",
"updatedAt": "2026-04-10T10:01:02Z",
"startedAt": "2026-04-10T10:00:01Z",
"completedAt": "2026-04-10T10:01:02Z"
}
Go SDK
task, err := client.GetTask(ctx, taskID)
task, err = client.WaitTask(ctx, taskID, 2*time.Second)
if task.IsSuccess() {
// task.ResultData
}
// Записывать к себе — только когда департамент это разрешает
if task.IsReadyToApply {
// apply 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- Пайплайн продолжается; перевод может перезапуститься с новым контекстом
Альтернатива без вебхука (pull)
Если поднять приёмник вебхуков невозможно, опрашивайте GET /internal/api/v1/tasks/:id: поле
ответа contextRequest (D10) несёт тот же активный запрос контекста — null, если его
нет — так что WaitTask или ручной polling может обнаружить ожидающий вопрос без подписки на
вебхуки. Ответ отправляется тем же способом — POST .../tasks/{taskId}/context-response. Про
форму поля — раздел Задачи (Tasks).
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 (локализация изображений)
Асинхронное распознавание разметки текста /internal/api/v1/staticloc/*: отправьте изображение и получите весь текст на нём со шрифтом, цветом и положением в пикселях, плюс области, покрывающие текст (маска для удаления). Модель и промпт один раз настраивает администратор организации.
| Method | Path | Назначение |
|---|---|---|
POST |
/internal/api/v1/staticloc/text-layout |
Загрузка изображения (multipart-поле image) и запуск задачи |
GET |
/internal/api/v1/staticloc/text-layout/tasks/:id |
Опрос задачи |
GET |
/internal/api/v1/staticloc/tasks |
Список (cursor; textCount вместо полного результата) |
GET |
/internal/api/v1/staticloc/tasks/:id |
Задача |
Терминальные статусы: succeeded | failed | canceled. Организация без настройки получает 422 {"error":"text_layout_not_configured"}; устаревшая настройка (движок выключен, у промпта снят тег, доступ к движку отозван) — 422 {"error":"text_layout_misconfigured"}.
Ответ на создание (202):
{
"taskId": "8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13",
"status": "new",
"pollUrl": "/internal/api/v1/staticloc/text-layout/tasks/8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13"
}
Опрос — succeeded:
{
"taskId": "8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13",
"type": "text_layout",
"status": "succeeded",
"createdAt": "2026-09-30T11:00:00Z",
"updatedAt": "2026-09-30T11:00:18Z",
"result": {
"image": { "width": 1200, "height": 628 },
"texts": [
{ "index": 0, "text": "SUMMER\nSALE", "x": 94, "y": 74, "fontFamily": "Montserrat",
"fontWeight": 800, "fontStyle": "normal", "fontSize": 59, "color": "#FFFFFF" }
],
"boxes": [
{ "textIndex": 0, "x": 94, "y": 74, "width": 530, "height": 59 },
{ "textIndex": 0, "x": 94, "y": 139, "width": 422, "height": 59 }
],
"warnings": []
}
}
Список задач:
{
"tasks": [ /* TaskResponse с textCount… */ ],
"nextCursor": "2026-09-30T11:00:00.123Z|8f0c6f0e-6d0a-4a7e-9b1f-2d4b6a8e0c13"
}
SDK: CreateStaticlocTextLayout, ListStaticlocTasks, GetStaticlocTask, WaitStaticlocTask.
Удаление текста
Асинхронное удаление текста: отправьте изображение (и, по желанию, маску) и получите то же изображение без текста в исходном формате (PNG → PNG, JPEG → JPEG, WebP → PNG) и размере. Модель, промпт, качество и точность следования исходнику один раз настраивает администратор организации.
| Method | Path | Назначение |
|---|---|---|
POST |
/internal/api/v1/staticloc/text-erase |
Загрузка image (png/jpeg/webp, ≤10 МБ, каждая сторона ≤4096 px) и необязательной mask, запуск задачи |
GET |
/internal/api/v1/staticloc/text-erase/tasks/:id |
Опрос задачи удаления текста |
Маска (необязательна): PNG ровно размера изображения. Если в ней есть прозрачные пиксели, область удаления задают прозрачные (соглашение OpenAI); полностью непрозрачная маска задаёт её белыми пикселями. С маской меняться может только эта область — пиксели вне её копируются из оригинала. Без маски (maskMode: "none") модель удаляет наложенный дизайнерский текст со всего изображения.
Ошибки содержат стабильный код в error и message: 400 invalid_image, image_too_large, mask_unsupported_type, mask_dimensions_mismatch, mask_empty, invalid_mask; 422 text_erase_not_configured, text_erase_misconfigured; 503 queue_unavailable.
Ответ на создание (202):
{
"taskId": "3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a",
"status": "new",
"maskMode": "masked",
"pollUrl": "/internal/api/v1/staticloc/text-erase/tasks/3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a"
}
Опрос — succeeded (url подписан и действует expiresInSec секунд; за свежей ссылкой опросите снова):
{
"taskId": "3b9d1c4e-0f7a-4c2d-8e6b-5a1f2d3c4b5a",
"type": "text_erase",
"status": "succeeded",
"maskMode": "masked",
"createdAt": "2026-09-30T11:00:00Z",
"updatedAt": "2026-09-30T11:00:41Z",
"result": {
"imageId": "0e5c7a1d-2b3f-5d6e-9f8a-7b6c5d4e3f2a",
"url": "https://…",
"mimeType": "image/png",
"width": 1200,
"height": 628,
"expiresInSec": 900
}
}
Упавшая задача содержит errorType (например, content_rejected, mask_empty, edit_rejected, transient_exhausted) и обобщённый errorMessage. Список задач (GET /staticloc/tasks?type=text_erase) показывает задачи удаления текста с их maskMode.
SDK: CreateStaticlocTextErase, GetStaticlocTextEraseTask.
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 |
Готова порция публикации или задача достигла финального статуса, допускающего доставку |
context_request |
Нужен контекст; ответ через context-response |
task.status_changed |
Задача переходит в новый статус (включая создание) |
task.tms.assigned |
Задача отправлена в TMS |
task.tms.step_completed |
Лингвист завершил шаг workflow |
task.tms.language_approved |
Целевой язык одобрен |
task.tms.language_reopened |
Целевой язык открыт заново |
task.tms.withdrawn |
Назначение в TMS отозвано |
review.completed |
Завершилось ревью (task-level или LQA) |
Отдельного поля task.completed / event нет — всё передаётся через type. Lifecycle-события
задачи (task.status_changed, task.tms.*, review.completed) подробно описаны, с формой data
для каждого типа, в разделе Task lifecycle события; они отправляются на том же
подписанном канале, описанном на этой странице, и по умолчанию включены для каждого департамента
(отключение через список).
Translation webhook — plain (один язык)
Envelope camelCase; nested result — languageResults.
{
"type": "translation",
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "done",
"resultVersion": 1,
"final": true,
"department": "Product",
"sourceLang": "en",
"targetLang": "ru",
"targetLangs": ["ru"],
"completedAt": "2026-04-10T09:12:08Z",
"test": false,
"metadata": {
"ticket": "LOC-1842",
"clientRequestId": "req-checkout-2.4"
},
"publication": {
"mode": "per_task",
"portion": 0,
"languages": {
"ru": { "state": "complete", "published": 1, "pendingReview": 0, "reviewed": 0 }
},
"pendingSegments": [],
"decisionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?route=review"
},
"result": {
"languageResults": {
"ru": {
"items": [
{
"type": "plain",
"content": "Завершите покупку"
}
],
"keySources": {},
"source": "translated",
"hasError": false,
"error": "",
"metadata": {
"sourceLang": "en",
"targetLang": "ru",
"itemsCount": 1,
"stringsCount": 1,
"processingTimeMs": 1250,
"processingInfo": {
"pipeline": "product-ui",
"engine": "openai",
"model": "gpt-4o"
}
}
}
},
"partialFailure": false,
"metadata": {}
}
}
Translation webhook — keyvalue multi-language
{
"type": "translation",
"taskId": "a0b1c2d3-e4f5-4678-89ab-cdef01234567",
"status": "ready",
"resultVersion": 1,
"final": true,
"department": "CMS - Configs",
"sourceLang": "en",
"targetLangs": ["de", "es", "fr"],
"completedAt": "2026-04-10T12:00:00Z",
"test": false,
"metadata": {
"configId": "3398",
"type": "config",
"stringIds": "[819793,819794,819795,819796]"
},
"publication": {
"mode": "per_task",
"portion": 0,
"languages": {
"de": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 },
"es": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 },
"fr": { "state": "complete", "published": 4, "pendingReview": 0, "reviewed": 0 }
},
"pendingSegments": [],
"decisionsUrl": "/internal/api/v1/tasks/a0b1c2d3-e4f5-4678-89ab-cdef01234567/segments?route=review"
},
"result": {
"languageResults": {
"de": {
"items": [
{
"type": "keyvalue",
"content": {
"welcome.title": "Willkommen",
"welcome.body": "Schön, dass du da bist",
"welcome.cta": "Loslegen",
"permissions.camera": "Kamerazugriff erlauben"
}
}
],
"keySources": {
"welcome.title": "translated",
"welcome.body": "translated",
"welcome.cta": "translated",
"permissions.camera": "translated"
},
"source": "translated",
"hasError": false,
"error": "",
"metadata": {
"sourceLang": "en",
"targetLang": "de",
"itemsCount": 1,
"stringsCount": 4,
"processingTimeMs": 416
}
},
"es": {
"items": [
{
"type": "keyvalue",
"content": {
"welcome.title": "Bienvenido",
"welcome.body": "Nos alegra que estés aquí",
"welcome.cta": "Empezar",
"permissions.camera": "Permitir acceso a la cámara"
}
}
],
"source": "translated",
"hasError": false,
"metadata": {
"sourceLang": "en",
"targetLang": "es",
"processingTimeMs": 421
}
},
"fr": {
"items": [
{
"type": "keyvalue",
"content": {
"welcome.title": "Bienvenue",
"welcome.body": "Ravi de vous voir ici",
"welcome.cta": "Commencer",
"permissions.camera": "Autoriser l'accès à la caméra"
}
}
],
"source": "translated",
"hasError": false,
"metadata": {
"sourceLang": "en",
"targetLang": "fr",
"processingTimeMs": 420
}
}
},
"partialFailure": false,
"metadata": {
"processingInfo": {
"pipeline": "cms-configs",
"engine": "openai",
"model": "gpt-4o"
},
"itemsCount": 3,
"stringsCount": 12,
"processingTimeMs": 535
}
}
}
Снимки публикации
В настройках департамента Публикация результата определяет, когда переводы публикуются:
| Вариант в UI | publication.mode |
Поведение |
|---|---|---|
| Вся задача сразу | per_task (по умолчанию) |
Результат задачи публикуется целиком; вебхук перевода содержит final: true |
| По языку | per_language |
Язык публикуется, когда в нём не осталось сегментов, ожидающих проверки |
| По сегменту | per_segment |
Прошедшие проверку сегменты публикуются, пока остальные ждут проверки |
Режим копируется в задачу при создании. По сегменту использует объём проверки
Только сегменты на проверке (narrow) или Все сегменты, прошедшие заблокированы (lock)
из профиля качества.
Каждый вебхук перевода содержит resultVersion, final и publication на верхнем уровне.
Каждая порция — полный снимок всего, что можно опубликовать сейчас, включая переводы
из предыдущих порций; это не дельта.
| Поле | Значение |
|---|---|
resultVersion |
Ревизия результата задачи; по ней решается, применять ли снимок |
final |
false, пока цикл публикации продолжается; true в последнем снимке цикла |
publication.mode |
per_task, per_language или per_segment |
publication.portion |
Порядковый номер порции публикации |
publication.languages |
Карта по целевым языкам; каждое значение содержит state (pending, partial или complete) и счётчики published, pendingReview, reviewed |
publication.pendingSegments |
Авторитетный список ячеек, которые нельзя публиковать или нужно снять с публикации |
publication.decisionsUrl |
Путь API к сегментам задачи из группы «На проверке»; вызывайте с ключом, у которого есть scope tasks |
reviewed считает опубликованные ячейки, прошедшие проверку человеком. Каждый ожидающий
сегмент адресуется через lang, itemIndex с нуля и key. Его массив reasons содержит
layer, check, outcome и опциональный severity; даже пустой массив означает,
что ячейка не должна быть опубликована. final находится на верхнем уровне, вне publication.
Пример: одна немецкая ячейка опубликована, другая остаётся в группе «На проверке».
{
"type": "translation",
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "human_review",
"department": "Product",
"sourceLang": "en",
"targetLang": "de",
"targetLangs": ["de"],
"completedAt": "2026-09-30T09:12:08Z",
"resultVersion": 14,
"final": false,
"result": {
"languageResults": {
"de": {
"items": [
{ "type": "keyvalue", "content": { "checkout.title": "Zur Kasse" } }
],
"hasError": false
}
}
},
"publication": {
"mode": "per_segment",
"portion": 3,
"languages": {
"de": { "state": "partial", "published": 1, "pendingReview": 1, "reviewed": 0 }
},
"pendingSegments": [
{
"lang": "de",
"itemIndex": 0,
"key": "dose.warning",
"reasons": [
{ "layer": "l1", "check": "risk.medical_accuracy", "outcome": "veto", "severity": "critical" }
]
}
],
"decisionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?route=review"
},
"test": false
}
Доставка — как минимум один раз (at-least-once). Дедуплицируйте по webhook-id
и применяйте только resultVersion, больший последней применённой версии этой задачи.
Доставленные версии не уменьшаются, но промежуточные версии могут пропускаться:
не ждите последовательных номеров версий или порций.
Применяйте полный снимок и снимайте с публикации каждую ячейку из pendingSegments,
даже если предыдущий снимок её опубликовал. final: true закрывает этот цикл;
последующая правка начинает новый цикл с большим resultVersion.
Ловушка имён полей
| Поверхность | 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применять только большийresultVersion; читатьlanguageResults(илиitemsдля одного языка) как полный снимок - Снять с публикации каждую ячейку из
publication.pendingSegments, в том числе опубликованную ранее - Считать
final: trueпоследним снимком этого цикла; принимать последующие правки с большими версиями
Подписи вебхуков
Исходящие вебхуки следуют спецификации 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 |
GetTaskSegments |
GET /internal/api/v1/tasks/{id}/segments |
GetSegmentVersions |
GET /internal/api/v1/tasks/{id}/segments/{key}/versions |
ListLanguages |
GET /internal/api/v1/languages |
ListLanguagePairs |
GET /internal/api/v1/language-pairs |
ListTags |
GET /internal/api/v1/tags |
UpsertCMSConfig |
POST /internal/api/v1/cms/configs/upsert |
| Хелперы staticloc | /internal/api/v1/staticloc/* |
ListAIModules |
GET /internal/api/v1/ai-modules |
InvokeAIModule |
POST /internal/api/v1/ai-modules/{key}/invoke |
VerifyWebhookSignature / ParseWebhook / DecodeTranslationResult |
вебхуки |
Про GetTaskSegments / GetSegmentVersions и обработку ошибок (обе возвращают
sdk.ErrInvalidArgument при пустом обязательном аргументе) — раздел
Сегменты и история правок.
Про ListAIModules / InvokeAIModule — раздел AI-модули.
Примеры в модуле
example_translate.go, example_async_poll.go, example_webhook.go, example_files.go, example_cms.go, example_staticloc.go, example_discovery.go, example_additional_info.go, example_existing_translations.go, example_ai_modules.go
Ошибки
Неуспешные HTTP-ответы декодируются в *sdk.APIError с полями error (или code AI-модулей) и опциональным message; на 429 и 503 может быть задан 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
Task lifecycle события
Помимо translation и context_request (Webhooks), сервис отправляет семейство
событий жизненного цикла задачи на том же подписанном канале: тот же webhookUrl, та же
подпись Standard Webhooks, тот же журнал попыток, та же политика ретраев. Теперь webhook приходит
и когда задача двигается по kanban, отправляется в TMS, у неё завершается шаг лингвистом, или её
одобряют после ревью.
Реестр событий
type |
Когда происходит |
|---|---|
task.status_changed |
Любой переход статуса, включая создание (from: null) |
task.tms.assigned |
Создано назначение в TMS (задача отправлена в TMS) |
task.tms.step_completed |
Лингвист завершил шаг workflow |
task.tms.language_approved |
Целевой язык одобрен в workflow TMS |
task.tms.language_reopened |
Ранее одобренный целевой язык открыт заново |
task.tms.withdrawn |
Назначение в TMS отозвано |
review.completed |
Завершилось ревью (task-level или LQA), с прикреплённым результатом |
Отдельного типа task.failed нет — провал задачи проявляется как task.status_changed с
data.to: "failed".
Envelope
Каждое lifecycle-событие использует тот же camelCase envelope, что и translation/context_request,
плюс объект data, специфичный для типа события:
{
"type": "task.status_changed",
"eventId": "9f8c7b6a-1111-4222-8333-444444444444",
"occurredAt": "2026-09-09T10:00:00.123456Z",
"taskId": "550e8400-e29b-41d4-a716-446655440000",
"department": "marketing",
"sourceLang": "en",
"targetLangs": ["de", "fr"],
"status": "ready",
"test": false,
"data": {
"from": "human_review",
"to": "ready",
"changedBy": "550e8400-e29b-41d4-a716-446655440099",
"note": null
}
}
| Поле | Примечания |
|---|---|
eventId |
UUID, уникален для каждого события; используйте для идемпотентности/dedupe вдобавок к webhook-id |
occurredAt |
UTC, RFC3339 с наносекундами |
status |
Статус задачи на момент доставки (snake enum) |
targetLang / targetLangs |
Сокращённая форма для одного языка / полный список — правило то же, что и у translation |
data |
Всегда объект (никогда не опускается); форма зависит от type — см. ниже |
data по типу события
task.status_changed
| Поле | Тип | Примечания |
|---|---|---|
from |
string | null | Предыдущий статус; null при создании задачи |
to |
string | Новый статус |
changedBy |
string (uuid) | null | Пользователь, изменивший статус; null для автоматических переходов |
note |
string | null | Опциональная заметка перехода |
task.tms.assigned, task.tms.step_completed, task.tms.language_approved, task.tms.language_reopened, task.tms.withdrawn
| Поле | Тип | Примечания |
|---|---|---|
assignmentId |
string | Id назначения в TMS |
targetLang |
string | Язык, к которому относится назначение |
step |
string | Шаг workflow |
wordCount |
number | Опускается, если неизвестен |
Коммерческие/вендорские поля (личность исполнителя, ставки, quote) на этом канале никогда не передаются.
review.completed
{
"type": "review.completed",
"eventId": "bbbbbbbb-1111-4222-8333-444444444444",
"occurredAt": "2026-09-09T11:00:00Z",
"taskId": "550e8400-e29b-41d4-a716-446655440000",
"department": "marketing",
"sourceLang": "en",
"targetLangs": ["ru"],
"status": "ready",
"test": false,
"data": {
"reviewKind": "task",
"approvedAt": "2026-09-09T10:59:00Z",
"approvedBy": "550e8400-e29b-41d4-a716-446655440099",
"editedSegments": {"ru": 3}
},
"result": {
"languageResults": {
"ru": {
"items": [{"type": "plain", "content": "Привет"}],
"hasError": false
}
},
"partialFailure": false
},
"metadata": {}
}
| Поле | Тип | Примечания |
|---|---|---|
data.reviewKind |
string | "task" или "lqa" |
data.approvedAt |
string | RFC3339 |
data.approvedBy |
string (uuid) | Id ревьюера |
data.editedSegments |
object | Счётчик отредактированных сегментов по языкам |
result |
object | null | Та же форма, что у результата translation webhook (languageResults); null, если результат недоступен — поле никогда не опускается |
metadata |
object | Опускается, если пусто |
Ручной перевод задачи в ready на kanban (без прохождения через действие ревью) шлёт только
task.status_changed, без review.completed.
Подписка — по умолчанию ВСЕ, отключение по департаменту
По умолчанию каждый департамент получает все подписываемые типы событий (translation,
task.status_changed, task.tms.*, review.completed; context_request отправляется всегда и
не входит в список для отключения). Задайте явный allow-list в настройках департамента, чтобы
получать только часть событий. Если ваш приёмник падает на незнакомом type, либо обновите его,
чтобы он игнорировал неизвестные типы, либо отключите семейства событий, которые пока не
обрабатываете.
Порядок доставки и ретраи
- Порядок доставки между разными типами событий не гарантируется — если важен строгий
порядок, сортируйте по
occurredAt. - Та же политика ретраев, что у
translation: до 5 ретраев (6 попыток всего) с backoff; ретраится и не-2xx, и transport-ошибки. - События
task.tms.*доставляются через внутренний outbox по периодическому тику (несколько секунд), поэтому могут немного отставать от действия в TMS, которое их породило.
Чеклист приёмника (дополнение)
- Обрабатывайте весь набор
type, а не толькоtranslation/context_request - Для
review.completedобрабатывайтеresult: null(у задачи ещё нет сохранённого результата) - Dedupe по
eventId(на событие) в дополнение кwebhook-id(на доставку) - Незнакомые значения
typeсчитайте forward-compatible no-op (подтверждайте, не падайте)
Сегменты и история правок
Доступ на чтение к отдельным переводимым ячейкам задачи («сегментам») и их полной истории
правок: исходный текст, изначальный машинный перевод, текущее значение (с наложенной
утверждённой правкой), решения гейта, состояние публикации, best-effort provenance по TM/LQA и все версии, через которые прошёл
сегмент. Оба эндпоинта — в том же scope tasks, что и GET /internal/api/v1/tasks/:id.
| Метод | Путь | Описание |
|---|---|---|
GET |
/internal/api/v1/tasks/:id/segments |
Список всех сегментов задачи |
GET |
/internal/api/v1/tasks/:id/segments/:key/versions |
Цепочка версий одного сегмента |
Идентичность сегмента
Сегмент адресуется тройкой (lang, itemIndex, key):
lang— код целевого языкаitemIndex— индекс item'а запроса, которому принадлежит ключ (с 0). Нужен потому что задача типаkeyvalueможет содержать несколько items на один язык, и один и тот же ключ (напримерhero.title) может законно повторяться в разных items —itemIndexразрешает эту неоднозначность.key— ключ ячейки (для items типаkeyvalue/json) или синтетический ключ дляplain
key может содержать |, /, % или Unicode-символы и должен быть URL-экранирован как
сегмент пути при прямом вызове эндпоинта версий (Go SDK делает это сам). Поле versionsUrl
каждого сегмента — уже готовая экранированная ссылка.
Список сегментов
GET /internal/api/v1/tasks/:id/segments
| Query-параметр | Обяз. | Заметки |
|---|---|---|
lang |
нет | Фильтр по одному целевому языку |
route |
нет | publish или review (включая решения reviewed) |
publication |
нет | published, pending_review, reviewed или unpublished |
layer |
нет | Слой остановки: l0, l1, l2 или l3 |
include |
нет | checks для результатов отдельных проверок |
cursor |
нет | Opaque-курсор из nextCursor предыдущей страницы |
limit |
нет | Размер страницы; по умолчанию 200, максимум 500 |
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?lang=de&limit=100" \
-H "Authorization: Bearer dk_your_department_api_key"
{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"items": [
{
"lang": "de",
"itemIndex": 0,
"key": "checkout.title",
"itemType": "keyvalue",
"source": "Checkout",
"initialMt": "Kasse",
"current": "Zur Kasse",
"keySource": "content",
"decision": {
"route": "review",
"status": "reviewed",
"decidedAt": "2026-04-10T10:05:00Z",
"reasons": [
{ "layer": "", "check": "", "outcome": "human_review", "veto": false }
],
"overridden": false
},
"publication": {
"state": "reviewed",
"resultVersion": 14,
"publishedAt": "2026-04-10T10:05:00Z",
"deliveredAt": "2026-04-10T10:05:01Z",
"value": "Zur Kasse"
},
"versionsUrl": "/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments/checkout.title/versions?lang=de&itemIndex=0",
"tm": {
"tier": "fuzzy",
"score": 1,
"matchPercent": 87,
"memoryId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"locked": false
},
"lqa": [
{
"dimension": "accuracy",
"category": "mistranslation",
"severity": "minor",
"status": "open",
"explanation": "Slightly literal phrasing.",
"suggestedFix": "Zur Kasse gehen"
}
]
}
],
"nextCursor": "eyJsYW5nIjoiZGUiLCJpdGVtSW5kZXgiOjAsImtleSI6ImNoZWNrb3V0LnRpdGxlIn0",
"warnings": []
}
| Поле | Заметки |
|---|---|
source |
Текст на исходном языке для этой ячейки (пусто, если исходный item не сматчился) |
initialMt |
Значение ячейки сразу после завершения пайплайна — до любых правок человеком |
current |
Текущее значение с наложенной последней утверждённой правкой (иначе — сырой вывод MT) |
keySource |
Источник ключа (например content), если он есть в сохранённой карте key-sources задачи, иначе пусто |
tm |
Best-effort информация о совпадении с TM — отсутствует, если неизвестна (см. ниже) |
provenance |
Опциональный pass-through provenance нод пайплайна (K-07); отсутствует, если нет данных |
lqa |
LQA-находки для этой ячейки из самой свежей завершённой full-LQA оценки по языку (пустой массив, никогда не omitted) |
decision |
Решение гейта пайплайна; отсутствует, если решения нет (см. ниже) |
publication |
Состояние публикации и опциональные опубликованное значение/время; всегда присутствует |
versionsUrl |
Готовая экранированная ссылка на цепочку версий сегмента |
warnings |
См. «Предупреждения» ниже |
Задача без сохранённого результата возвращает items: []. Фильтр lang, не давший ни одной
ячейки, также возвращает items: [] (это не ошибка).
Фильтры объединяются и применяются до постраничной выдачи. Некорректные route,
publication, layer или include возвращают HTTP 400 invalid_filter.
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments?lang=de&route=review&publication=pending_review&layer=l1&include=checks" \
-H "Authorization: Bearer dk_your_department_api_key"
Решения гейта и публикация
У каждой ячейки есть объект publication. Её decision — решение гейта пайплайна;
оно отсутствует, если пайплайн ещё не создал решение для этой ячейки. Ручные прогоны
качества носят рекомендательный характер и не меняют маршрут и не публикуют переводы.
Поле decision |
Значение |
|---|---|
route |
publish для passed или locked; review для needs_review или reviewed |
status |
passed, needs_review, locked или reviewed |
layer |
Опциональный слой остановки: l0 (L0 Формат), l1 (L1 Критические риски), l2 (L2 Критические ошибки) или l3 (L3 Балл качества) |
profileVersionId |
Опциональный UUID использованной опубликованной версии профиля качества |
decidedAt |
Время решения (ISO 8601) |
score |
Опциональный балл сегмента |
reasons |
Массив причин; присутствует, даже если пуст |
checks |
Результаты отдельных проверок, присутствуют только с include=checks; пустой массив допустим |
overridden |
Переопределил ли менеджер решение |
К публикации и На проверке — группы решений гейта. route=review включает
уже проверенные ячейки reviewed; для решений, которые ещё требуют действий,
проверяйте decision.status == needs_review. Маршрут «К публикации» описывает результат
гейта; publication.state показывает, опубликована ли ячейка.
Поле decision.reasons[] |
Значение |
|---|---|
layer |
Слой, к которому относится причина |
check |
Идентификатор проверки |
outcome |
Вид причины: например, fail, veto, critical_finding, below_threshold, check_failed, missing, no_verdict, manual, human_review или qe |
severity |
Опциональный neutral, minor, major или critical |
findingId |
Опциональный UUID находки |
detail |
Опциональное объяснение |
score |
Опциональный ненулевой балл |
veto |
true для причин на l0, l1 или l2; иначе false |
Поле decision.checks[] |
Значение |
|---|---|
check |
Идентификатор проверки |
layer |
Слой проверки |
status |
ok, failed, not_applicable или skipped |
verdict |
Опциональный pass или fail |
score |
Опциональный балл проверки |
Поле publication |
Значение |
|---|---|
state |
published (Опубликован), pending_review (Ждёт ревью), reviewed (Проверен и опубликован) или unpublished |
resultVersion |
Опциональная первая ревизия результата, в которой ячейка была опубликована; отличается от текущей ревизии задачи |
publishedAt |
Опциональное время первой публикации |
deliveredAt |
Время успешной доставки вебхука; отсутствует до подтверждения ответом 2xx |
revisedAt |
Опциональное время изменения опубликованного значения |
value |
Опциональное значение из записи публикации; импортируйте его только при состоянии published или reviewed |
current включает утверждённые правки и может отличаться от publication.value.
Импортируйте только ячейки со состоянием публикации published или reviewed.
Время публикации — в ISO 8601. published описывает допуск к публикации и запись о ней;
deliveredAt подтверждает получение вебхука.
TM best-effort — заметка (D3)
tm отражает processingInfo.tmApplied уровня задачи на момент выполнения пайплайна. Для
задачи keyvalue с несколькими items здесь действует семантика «последний пишет
поверх» — если два item'а затронули один ключ, на уровне задачи сохраняется TM-информация
только последнего, поэтому tm может отсутствовать или относиться к другому item'у, чем
запрошенный. Считайте tm информативным, а не авторитетным полем, пока не появится
поэлементное хранение TM. Если ничего не известно, tm отсутствует полностью.
Предупреждения
warnings — дедуплицированный массив в фиксированном порядке:
| Предупреждение | Значение |
|---|---|
history_unavailable |
Не удалось загрузить историю правок; current/initialMt откатываются к сырому сохранённому результату |
lqa_unavailable |
Не удалось загрузить LQA-находки хотя бы для одного языка; lqa для этих ячеек пуст |
publication_unavailable |
Не удалось загрузить данные публикации |
Цепочка версий сегмента
GET /internal/api/v1/tasks/:id/segments/:key/versions
| Query-параметр | Обяз. | Заметки |
|---|---|---|
lang |
да | Целевой язык |
itemIndex |
нет | По умолчанию 0 — см. «Идентичность сегмента» выше |
cursor |
нет | Opaque-курсор из nextCursor предыдущей страницы |
limit |
нет | Размер страницы; по умолчанию 200, максимум 500. Учитывает синтетическую версию mt на первой странице |
curl -sS "https://krakenloc.com/internal/api/v1/tasks/7c9e6679-7425-40de-944b-e07fc1f90ae7/segments/checkout.title/versions?lang=de&itemIndex=0" \
-H "Authorization: Bearer dk_your_department_api_key"
{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"lang": "de",
"itemIndex": 0,
"key": "checkout.title",
"versions": [
{
"n": 1,
"kind": "mt",
"value": "Kasse",
"at": "2026-04-10T09:12:08Z",
"author": { "kind": "system", "label": "pipeline" },
"note": null
},
{
"n": 2,
"kind": "edit",
"value": "Zur Kasse",
"at": "2026-04-10T10:02:14Z",
"author": { "kind": "user", "label": "[email protected]" },
"note": null
},
{
"n": 3,
"kind": "approve",
"value": "Zur Kasse",
"at": "2026-04-10T10:05:00Z",
"author": { "kind": "user", "label": "[email protected]" },
"note": null
}
],
"nextCursor": "",
"warnings": []
}
Виды версий
| Kind | От какого действия истории | value |
author |
|---|---|---|---|
mt |
Синтетическая — всегда n: 1, только на первой странице |
Значение сразу после работы пайплайна | {kind: "system", label: "pipeline"} |
edit |
created, value_changed |
Новое значение после правки | Автор правки (или import, см. ниже) |
approve |
review_approved |
Значение до ревью (сама аппрувка его не меняет) | Ревьюер |
reject |
review_rejected |
Значение до отклонения | Ревьюер |
reset |
review_reset |
Значение до сброса | Ревьюер |
apply |
applied |
Значение до применения | {kind: "system", label: "applied"} |
revert |
reverted, reverted_to_version |
Восстановленное значение (для legacy-строк — откат к original_value правки) | Пользователь, выполнивший revert |
Виды авторов
| Kind | Значение |
|---|---|
user |
Именованный человек-редактор или ревьюер — label содержит его email/идентификатор |
agency |
Редактор со стороны агентства — label всегда буквально "agency", реальная личность на этом канале никогда не раскрывается |
import |
Правка создана путём массового импорта (origin:import) — label содержит импортирующего пользователя, kind помечает источник как импорт |
system |
Автоматическое действие (изначальный вывод MT или шаг «apply») — без участия человека |
Постраничная выдача
Оба эндпоинта используют одно правило opaque-курсора: запрашивается limit (по умолчанию
200, максимум 500), внутри читается limit + 1 строк, возвращается limit и nextCursor
устанавливается, если есть ещё. Курсор одного сегмента/списка нельзя переиспользовать для
другого — сервер вернёт 400 invalid_cursor, если координата не совпадает.
Специфика эндпоинта версий: синтетическая версия mt добавляется только на первую
страницу и учитывается в limit — например, limit=50 на первой странице вернёт mt
плюс до 49 реальных записей истории. Номера версий (n) — устойчивые порядковые номера,
продолжающиеся между страницами (они никогда не начинаются заново с n: 1 на следующей
странице).
Ошибки
| Эндпоинт | Статус | error |
Причина |
|---|---|---|---|
| Список | 400 | invalid_id |
ID задачи — не UUID |
| Список | 400 | invalid_cursor |
Курсор некорректен или от другой координаты |
| Список | 400 | invalid_limit |
limit не целое число, <= 0 или > 500 |
| Список | 400 | invalid_filter |
Неизвестное значение route, publication, layer или include |
| Список | 404 | task_not_found |
Задача не найдена или принадлежит другому org/product |
| Список | 500 | invalid_task_result |
Не удалось декодировать сохранённый результат задачи |
| Список | 500 | decision_unavailable |
Не удалось загрузить решения гейта пайплайна |
| Версии | 400 | invalid_id | missing_lang | invalid_item_index | invalid_cursor | invalid_limit |
См. выше, плюс обязательный lang или нецелый itemIndex |
| Версии | 404 | task_not_found | segment_not_found |
Задача не найдена/чужая, либо ни одна ячейка не совпала по (lang, itemIndex, key) |
| Версии | 500 | invalid_task_result |
Не удалось декодировать сохранённый результат задачи |
Go SDK
// Список всех немецких сегментов.
page, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{Lang: "de"})
for _, seg := range page.Items {
fmt.Println(seg.Key, seg.Current)
}
// Inspect German L1 cells waiting for review, including each check result.
review, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{
Lang: "de", Route: "review", Publication: "pending_review", Layer: "l1", IncludeChecks: true,
})
_ = review
// Следующая страница по курсору.
if page.NextCursor != "" {
next, err := client.GetTaskSegments(ctx, taskID, sdk.SegmentsQuery{Lang: "de", Cursor: page.NextCursor})
_ = next
}
// Полная цепочка версий одного сегмента.
versions, err := client.GetSegmentVersions(ctx, taskID, "de", 0, "checkout.title", "", 0)
for _, v := range versions.Versions {
fmt.Println(v.N, v.Kind, v.Author.Kind, v.Value)
}
GetTaskSegments/GetSegmentVersions возвращают sdk.ErrInvalidArgument (проверяется через
errors.Is), если обязательный аргумент (ID задачи, lang, key) пуст — запрос в этом
случае не отправляется. key экранируется клиентом сам; limit == 0 опускает
query-параметр (применяется дефолт сервера).
См. Go SDK — таблица всех методов, и Ответ с контекстом — про
contextRequest в ответе задачи.
Provenance промптов и моделей (K-07)
Для каждой текстовой LLM-ноды пайплайна (mt, ai_agent, lqa) оркестратор записывает
что реально выполнилось: какая версия промпта, какая модель и что произошло. Это
историческая правда об исполнении, а не реконструкция по текущей конфигурации пайплайна —
пайплайн может измениться после того, как задача выполнилась, а provenance по-прежнему
отражает то, что использовала конкретная задача.
Где искать эти данные
- Результат задачи —
resultData.metadata.processingInfo.provenanceу задачи в статусеdone(или частично выполненной), накапливается по всем языкам и items, через которые прошла задача (см. Tasks). - Сегменты —
provenanceв каждой строкеGET /internal/api/v1/tasks/:id/segments— это сквозная передача тех же данных, ограниченная(lang, itemIndex)этого сегмента (см. Segments); отсутствует, когда неизвестно — как иtm. - Аналитические таблицы (внутренние; expand-only колонки, добавлены аддитивно — ничего
существующего не переименовано и не удалено) —
provider_usage_logs.prompt_version_idиpost_edit_analytics.translation_prompt_version_idнесут один и тот жеpromptVersionId, так что расходы на LLM и метрики качества пост-редактирования можно связать с точной версией промпта, которая произвела перевод.
Формат
{
"provenance": {
"nodes": [
{
"executionId": "b7c1a9f0-2e34-4b3f-9b3d-2b7f6a9f0c11",
"targetLang": "de",
"itemIndex": 0,
"nodeId": "translate",
"nodeType": "ai_agent",
"stepName": "Translate",
"status": "ok",
"promptId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"promptVersionId": "c3f1e9a0-6b2d-4e1a-9c3e-7a1b2c3d4e5f",
"promptVersion": 4,
"promptName": "Checkout translator",
"promptKey": "checkout_translate",
"provider": "openai",
"model": "gpt-4o",
"modelSource": "applied",
"temperature": 0.3,
"reasoningEffort": null
}
],
"primary": {
"executionId": "b7c1a9f0-2e34-4b3f-9b3d-2b7f6a9f0c11",
"targetLang": "de",
"itemIndex": 0,
"nodeId": "translate",
"nodeType": "ai_agent",
"status": "ok",
"promptVersionId": "c3f1e9a0-6b2d-4e1a-9c3e-7a1b2c3d4e5f"
}
}
}
| Поле | Примечание |
|---|---|
nodes |
По одной записи на выполнение ноды — каждая нода mt/ai_agent/lqa, которая запускалась, включая упавшие или пропущенные. Никогда не опускается; [], если ничего ещё не выполнялось. |
nodes[].status |
ok | error | skipped_no_version (не была настроена версия промпта для запуска) | skipped_unavailable (продюсер ноды не был подключён для этой задачи) |
nodes[].promptId / promptVersionId |
Точный промпт и версия, которые использовались. promptVersionId равен null, если нода так и не определила версию (например, skipped_no_version) — без отката на «текущую версию промпта прямо сейчас». |
nodes[].model / modelSource |
См. Применённая vs настроенная ниже |
nodes[].itemIndex |
null для записи, общей для языка (не по item); задача keyvalue/json с несколькими items проставляет значение по item |
primary |
Нода, которую потребитель должен считать главным шагом перевода для данного (targetLang, itemIndex) — последняя нода ai_agent, либо последняя нода mt, если шага ai_agent не было. null, если ничего не выполнялось. |
Применённая vs настроенная модель
model — это модель, которая реально произвела ответ, когда оркестратор может её определить
(modelSource: "applied"). Некоторые пути падают до того, как модель ответила, либо провайдер
не сообщает, какая модель обслужила запрос — в этом случае model откатывается на то, что
было настроено на ноде/движке (modelSource: "configured"), что может не совпадать с тем,
что реально выполнилось (например, алиас модели у провайдера мог разрешиться в другую
базовую модель). Считайте modelSource: "configured" best-effort-значением, а не строгой
гарантией.
promptVersionId как стабильный ключ связи
Один и тот же UUID встречается в трёх местах для одного события перевода:
resultData...provenance.nodes[].promptVersionId(или сквозная передача в сегменте)provider_usage_logs.prompt_version_id— LLM-вызов, потребивший токеныpost_edit_analytics.translation_prompt_version_id— если сегмент позже был отредактирован человеком, строка, оценивающая правку относительно записанной primary-ноды для(targetLang, itemIndex)
post_edit_analytics.prompt_version_id (без префикса translation_) — это другой id:
промпт классификатора, разметившего тип правки, а не промпт перевода. Не путайте их при join.
promptVersionId может устареть: если промпт или конкретная версия позже удалены или
откачены, историческое provenance по-прежнему показывает записанный id (это то, что реально
выполнилось), но поиск по нему сейчас вернёт not-found. Админский просмотр цепочки промптов
(GET /internal/admin/v1/tasks/:id/prompt-chain) явно отражает это как "missing": true на
соответствующем шаге вместо того, чтобы молча его отбросить или подставить другую версию.
Управление версиями промптов
Контент и версионирование промптов управляются через админ-консоль, а не через этот API — для этих маршрутов нужна аутентифицированная админ-сессия (cookie), а не API-ключ департамента:
| Метод | Путь | Право доступа | Назначение |
|---|---|---|---|
GET |
/internal/admin/v1/prompts/:id/versions |
prompts.view |
Список версий промпта |
POST |
/internal/admin/v1/prompts/:id/versions |
prompts.create |
Опубликовать новую версию |
POST |
/internal/admin/v1/prompts/:id/versions/:version/revert |
prompts.edit |
Откатиться к более ранней версии |
GET |
/internal/admin/v1/tasks/:id/prompt-chain |
prompt_proposals.propose |
Разрешённая цепочка промптов одной задачи, сгруппированная по execution — записанное provenance, если доступно, иначе реконструкция по текущему пайплайну |
Публикация новой версии не меняет задним числом provenance, уже записанное на прошлых задачах — оно по-прежнему указывает на версию, которая их реально выполнила.
AI-модули
Опубликованные AI-модули прередактуры, которые можно вызвать ключом департамента. Ключу нужен scope ai_modules:invoke, модуль должен быть в allowlist ключа, а открытие в API — одобрено. Модуль, который ключ вызвать не может, в discovery просто отсутствует.
| Method | Path | Scope |
|---|---|---|
GET |
/internal/api/v1/ai-modules |
ai_modules:invoke — только то, что этому ключу можно вызвать |
POST |
/internal/api/v1/ai-modules/{key}/invoke |
ai_modules:invoke — запуск опубликованного снимка |
Сервер не читает surface и run.* из тела: их ставит маршрут. Выполняется только опубликованный неизменяемый снимок.
SDK: ListAIModules, InvokeAIModule.
Три якоря
Якорь задаёт тело вызова. Сервер никогда не меняет его молча.
| Тело | Якорь | Обязательно кроме targetLang |
|---|---|---|
задан taskId |
task |
rowKey — строка задачи |
задан cmsConfigId |
cms |
rowKey — строка CMS |
| ни того, ни другого | text |
text — исходная строка |
и taskId, и cmsConfigId |
— | 400 ambiguous_anchor |
Несуществующая задача/CMS-конфиг и чужие отвечают одинаковым 404 (task_not_found / cms_config_not_found).
Discovery
curl -sS "https://krakenloc.com/internal/api/v1/ai-modules" \
-H "Authorization: Bearer dk_your_department_api_key"
Ответ:
{
"items": [
{
"key": "shorten",
"name": "Shorten",
"version": 3,
"interfaceVersion": 2,
"maxVariants": 3,
"acceptsInline": ["description", "maxLength"],
"params": [
{
"name": "level",
"type": "enum",
"exposure": "cell_toolbar",
"default": "b1",
"options": [{"value": "b1", "label": "B1"}]
}
],
"modes": [
{
"mode": "api_task",
"anchor": "task",
"state": "full",
"losses": [],
"requires": []
},
{
"mode": "api_text",
"anchor": "text",
"state": "requires",
"losses": [{"source": "tm", "reason": "no_anchor", "impact": "skipped"}],
"requires": ["description", "maxLength"]
}
]
}
]
}
items — [] (HTTP 200), если поверхность API выключена у организации, у ключа нет открытых опубликованных модулей или вызывающий не идентифицирован. Discovery не показывает модуль в pending, выключенный, неопубликованный или закрытый для ключа.
Каждый элемент modes[] — режим API, который открывает экспозиция (api_task, api_cms, api_text). state — full, degraded, unavailable или requires. losses — источники, которые не попадут в промпт; requires / acceptsInline — inline-поля плана. Имена параметров — авторские и идут как есть. В params — только параметры, которые может задать вызов (exposure — cell_toolbar или run_dialog); параметры, зафиксированные автором, не показываются.
Вызов — задача + строка
curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
-H "Authorization: Bearer dk_your_department_api_key" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"rowKey": "checkout.cta",
"sourceLang": "en",
"targetLang": "ru-RU",
"currentText": "Перейти к оформлению",
"maxVariants": 2
}'
Вызов — CMS-конфиг + строка
curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
-H "Authorization: Bearer dk_your_department_api_key" \
-H "Content-Type: application/json" \
-d '{
"cmsConfigId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"rowKey": "checkout.cta",
"sourceLang": "en",
"targetLang": "ru-RU"
}'
Вызов — просто текст
targetLang обязателен всегда. На якоре text обязателен text. Необязательный context — недоверенный inline (массивы, не карты, для данных с ключом строки).
curl -sS -X POST "https://krakenloc.com/internal/api/v1/ai-modules/shorten/invoke" \
-H "Authorization: Bearer dk_your_department_api_key" \
-H "Content-Type: application/json" \
-d '{
"text": "Proceed to checkout",
"currentText": "Перейти к оформлению заказа",
"sourceLang": "en",
"targetLang": "ru-RU",
"params": {"level": "b1"},
"maxVariants": 2,
"expectInterfaceVersion": 2,
"strictContext": false,
"context": {
"description": "Primary checkout button",
"maxLength": 24,
"uiType": "button",
"styleNotes": "Keep the verb.",
"metadata": {"screen": "checkout"}
}
}'
Ответ (только поля результата — содержимого источников и trace в ответе нет):
{
"invocationId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"module": {"key": "shorten", "version": 3, "interfaceVersion": 2},
"status": "changed",
"text": "Оформить заказ",
"requestId": "fcb18b18-a335-4d8b-8fc4-42e85f211e49",
"anchor": "text",
"quality": "normal",
"edits": [
{
"from": "Перейти к оформлению заказа",
"to": "Оформить заказ",
"category": "length"
}
],
"alternatives": ["К оформлению"],
"diff": [
{"op": "delete", "text": "Перейти к "},
{"op": "equal", "text": "оформлению"},
{"op": "insert", "text": "Оформить заказ"}
],
"usedSources": [{"id": "key_metadata", "origin": "inline"}],
"degraded": [{"source": "tm", "reason": "no_anchor", "retryable": false}],
"usage": {
"model": "gpt-4.1-mini",
"inputTokens": 420,
"outputTokens": 48,
"latencyMs": 890
}
}
usedSources и degraded несут только идентификаторы и причины. notes и edits[].reason не отдаются, пока экспозиция модуля не включит объяснения (explain=false по умолчанию). invocationId — строка журнала; на этом публичном пути null не используется.
status — changed, no_change или cannot_comply. quality — normal или low_context. diff — пословный diff текущего значения и text; пробелы входят в кусок.
Нехватка данных: degraded и insufficient_context
Вызов может завершиться успешно, если необязательные источники не пришли. Они попадают в degraded (retryable — имеет ли смысл повторить). Discovery уже показывает ту же потерю на режиме (state: degraded).
Жёсткий порог качества срабатывает до вызова модели и не тарифицируется:
{
"code": "insufficient_context",
"message": "not enough context to run the module",
"details": {
"missing": ["tm"],
"unlockWith": ["taskId", "rowKey"],
"retryable": true
}
}
strictContext: true превращает мягкий порог в этот 422.
Обязательный источник без inline-данных — 422 required_source_unavailable (details.source, details.reason и details.missing = поля requires плана). Нет плана на названный якорь — 422 mode_not_supported (details.supportedAnchors).
Лимиты тела
| Лимит | Потолок |
|---|---|
| Тело запроса | 1 МиБ (больше → 413 invalid_request) |
context.neighbors |
50 |
context.siblings |
10 |
context.glossaryHints |
50 |
context.styleNotes |
500 символов |
context.metadata |
1500 символов (ключи + значения) |
context.description |
1000 символов |
context.uiType |
64 символа |
Строковое значение в params |
2000 символов (больше → 400 invalid_params) |
maxVariants |
1–5 |
context.screenshotUrl |
только абсолютный URL https |
Поле context сверх потолка — 400 invalid_request.
params сверяются со схемой параметров той версии, которую выполняет вызов, — последнего опубликованного снимка или закреплённого через moduleVersion — до чтения источников и вызова модели. Параметр, который вызов не прислал, получает значение по умолчанию модуля. Параметр с default: null обязателен: передайте непустое значение (модуль custom требует params.custom), иначе ответ — 400 invalid_params с причиной required. Любое расхождение — 400 invalid_params, а details.issues перечисляет все проблемы, отсортированные по имени параметра (не больше 20):
reason |
Когда |
|---|---|
unknown |
Модуль не объявляет параметр с таким именем |
fixed |
Параметр зафиксирован автором; вызов не может его задать |
null |
Значение — null |
wrong_type |
Значение не того типа: enum и string — JSON-строка, int — целое число, bool — true или false |
not_an_option |
Значения enum нет среди options[].value |
too_long |
Строка длиннее 2000 символов. Перед подсчётом удаляются управляющие символы (включая переводы строк), встраивания, переопределения и изоляты направления текста и тег-символы Unicode |
required |
У параметра нет значения по умолчанию, а вызов его не прислал или прислал строку без единого видимого символа: пустую или только из пробелов, управляющих или невидимых символов форматирования (например, пробелов нулевой ширины) |
Например, "params": {"level": "z9", "tone": "casual"} для модуля из примера discovery даёт:
{
"code": "invalid_params",
"message": "parameter \"level\": the value is not one of its options (and 1 more)",
"details": {
"issues": [
{"param": "level", "reason": "not_an_option"},
{"param": "tone", "reason": "unknown"}
]
}
}
message никогда не повторяет присланные значения.
targetLang и присланный sourceLang должны быть известными кодами языков. Подтеги региона, письменности и варианта допустимы (pt-BR, es-419, zh-Hant-TW, de-CH-1996); код с расширением или private-use частью (en-US-u-ca-gregory, de-t-en, en-x-test) — 400 invalid_request.
Несовпадение expectInterfaceVersion — 409 module_interface_changed. Старый снимок можно закрепить через moduleVersion (≥ 1).
Коды ошибок
Тело ошибки — { "code": "…", "message": "…", "details": {…} }. Отказы по частоте дополнительно ставят Retry-After (секунды).
| HTTP | code |
Когда |
|---|---|---|
| 400 | invalid_request |
Тело не разобрано, нет rowKey/text/targetLang, неизвестный код языка или код с расширением, поле context сверх потолка, плохой UUID, запрещённый screenshot URL |
| 400 | ambiguous_anchor |
И taskId, и cmsConfigId |
| 400 | invalid_params |
params не по схеме параметров выполняемой версии: неизвестное имя, фиксированный параметр, null, неверный тип, значение вне опций, строка длиннее 2000 символов, не задан обязательный параметр (details.issues) |
| 401 | — | Нет или неверный Bearer (middleware) |
| 403 | module_not_exposed |
Модуль не открыт этому ключу |
| 403 | engine_not_granted |
Движок не выдан организации |
| 403 | budget_exceeded |
Дневной USD-бюджет исчерпан (fail-closed) |
| 404 | module_not_found |
Неизвестный ключ модуля |
| 404 | task_not_found |
Нет задачи или чужая |
| 404 | cms_config_not_found |
Нет CMS-конфига или чужой |
| 409 | module_interface_changed |
expectInterfaceVersion не совпал |
| 413 | invalid_request |
Тело больше 1 МиБ |
| 422 | mode_not_supported |
Нет плана на этот якорь (details.supportedAnchors) |
| 422 | required_source_unavailable |
Нет обязательного источника (details.source, details.reason) |
| 422 | insufficient_context |
Жёсткий порог; не тарифицируется |
| 429 | rate_limited |
Лимит ключа, «модуль × ключ» или in-flight; Retry-After |
| 500 | internal_error |
Неожиданный сбой |
| 502 | llm_failed |
Сбой вызова модели |
| 503 | modules_disabled |
Поверхность API выключена у org (только invoke; discovery отдаёт []) |
| 503 | limiter_unavailable |
Проверка частоты не смогла выполниться (fail-closed); Retry-After |
| 504 | llm_timeout |
Таймаут модели |
Порядок проверок: экспозиция → частота ключа → лимит «модуль × ключ» → дневной бюджет → якорь → порог качества. Все 422 отдаются до вызова модели. Если лимитер не смог ответить, вызов отклоняется (503), а не пропускается.
Go SDK
listed, err := client.ListAIModules(ctx)
for _, m := range listed.Items {
fmt.Println(m.Key, m.Version, m.MaxVariants)
}
result, err := client.InvokeAIModule(ctx, "shorten", &sdk.AIModuleInvokeRequest{
Text: "Proceed to checkout",
SourceLang: "en",
TargetLang: "ru-RU",
})
fmt.Println(result.Text, result.Status, result.Anchor)
Неуспешные ответы модулей декодируются в *sdk.APIError: Err — проводное code, Message — message. На 429 и 503 RetryAfter берётся из Retry-After.