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

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

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

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

Обзор

Localization Service предоставляет REST API для программной локализации: отправка контента на перевод, опрос статуса задач, вебхуки, загрузка файлов, CMS-конфиги, статическая локализация изображений (staticloc) и вызов опубликованных 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
  • Для keyvalue existing может содержать подмножество ключей; остальное переводится
  • forceRetranslate: true игнорирует existing

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

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

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

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

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

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

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

{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "items": [
    { "type": "plain", "content": "Завершите покупку" },
    {
      "type": "keyvalue",
      "content": {
        "checkout.title": "Оформление заказа",
        "checkout.pay": "Оплатить",
        "checkout.secure": "Безопасная оплата",
        "checkout.tax_note": "Налог включён, где применимо"
      }
    }
  ],
  "meta": {
    "sourceLang": "en",
    "targetLang": "ru",
    "itemsCount": 2,
    "stringsCount": 5,
    "processingTimeMs": 1840,
    "tokensUsed": 312,
    "partialSuccess": false,
    "processingInfo": {
      "pipeline": "product-ui",
      "engine": "openai",
      "model": "gpt-4o",
      "schemaValidated": true
    }
  },
  "metadata": {
    "ticket": "LOC-1842"
  }
}

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

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

{
  "taskId": "parent-7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "taskIds": {
    "ru": "a1111111-1111-4111-8111-111111111111",
    "de": "a2222222-2222-4222-8222-222222222222",
    "fr": "a3333333-3333-4333-8333-333333333333"
  },
  "results": {
    "ru": {
      "taskId": "a1111111-1111-4111-8111-111111111111",
      "source": "mixed",
      "keySources": {
        "checkout.title": "provided",
        "checkout.pay": "provided",
        "checkout.secure": "translated",
        "checkout.tax_note": "translated"
      },
      "items": [
        { "type": "plain", "content": "Завершите покупку" },
        {
          "type": "keyvalue",
          "content": {
            "checkout.title": "Оформление заказа",
            "checkout.pay": "Оплатить",
            "checkout.secure": "Безопасная оплата",
            "checkout.tax_note": "Налог включён, где применимо"
          }
        }
      ],
      "meta": {
        "sourceLang": "en",
        "targetLang": "ru",
        "itemsCount": 2,
        "stringsCount": 5,
        "processingTimeMs": 1920
      }
    },
    "de": {
      "taskId": "a2222222-2222-4222-8222-222222222222",
      "source": "translated",
      "items": [
        { "type": "plain", "content": "Kauf abschließen" },
        {
          "type": "keyvalue",
          "content": {
            "checkout.title": "Zur Kasse",
            "checkout.pay": "Jetzt bezahlen",
            "checkout.secure": "Sichere Zahlung",
            "checkout.tax_note": "Steuern ggf. inklusive"
          }
        }
      ],
      "meta": {
        "sourceLang": "en",
        "targetLang": "de",
        "itemsCount": 2,
        "stringsCount": 5,
        "processingTimeMs": 2010
      }
    }
  },
  "metadata": {
    "ticket": "LOC-1842"
  }
}

source у языка: translated | provided | mixed.

curl — plain + webhook

curl -sS -X POST "https://krakenloc.com/internal/api/v1/translate" \
  -H "Authorization: Bearer dk_your_department_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "department": "Product",
    "sourceLang": "en",
    "targetLangs": ["ru", "de"],
    "webhookUrl": "https://hooks.acme-games.com/localization/kraken",
    "items": [
      { "type": "plain", "content": "Welcome back, commander." },
      {
        "type": "keyvalue",
        "content": {
          "home.play": "Play",
          "home.settings": "Settings",
          "home.shop": "Shop"
        }
      }
    ],
    "additionalInfo": {
      "title": "Home screen · EN→RU/DE",
      "externalRef": "LOC-1901"
    }
  }'

CMS write-back

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

{
  "department": "CMS - Configs",
  "sourceLang": "en",
  "targetLangs": ["ru", "de"],
  "cmsConfigId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "items": [
    {
      "type": "keyvalue",
      "content": {
        "welcome.title": "Welcome",
        "welcome.body": "Glad you are here"
      }
    }
  ]
}

Задачи (Tasks)

GET /internal/api/v1/tasks/:id

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

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

Статусы

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

Хелперы SDK (Go):

  • IsCompleted: ready | done | rejected | failed | canceled
  • IsSuccess: ready | done
  • IsFailed: failed | rejected | canceled

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

Поток

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

Альтернатива без вебхука (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_… в настройках департамента. См. Подписи.

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

  1. Быстро вернуть 2xx (обработка async)
  2. Verify signature на raw body
  3. Dedupe по webhook-id
  4. Branch по type
  5. Для translation применять только больший resultVersion; читать languageResults (или items для одного языка) как полный снимок
  6. Снять с публикации каждую ячейку из publication.pendingSegments, в том числе опубликованную ранее
  7. Считать final: true последним снимком этого цикла; принимать последующие правки с большими версиями

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

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

Алгоритм

  1. Требуются непустые webhook-id, webhook-timestamp, webhook-signature
  2. Отклонить, если |now − timestamp| > 5 минут (защита от replay)
  3. Секрет должен начинаться с whsec_; HMAC-ключ = Base64-decode части после whsec_
  4. signed_content = id + "." + timestamp + "." + raw_body
  5. expected = Base64(HMAC-SHA256(key, signed_content))
  6. webhook-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
  • Большие keyvalue batch могут резаться server-side
  • Лимит upload — reverse-proxy / конфиг окружения
  • Размер plain-текста — порядка десятков тысяч символов на item; крупные документы — несколько items или file_url

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

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

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

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

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

429 — rate limit

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

Неверный base path

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

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

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

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

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

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

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

В 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, которое их породило.

Чеклист приёмника (дополнение)

  1. Обрабатывайте весь набор type, а не только translation / context_request
  2. Для review.completed обрабатывайте result: null (у задачи ещё нет сохранённого результата)
  3. Dedupe по eventId (на событие) в дополнение к webhook-id (на доставку)
  4. Незнакомые значения 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 встречается в трёх местах для одного события перевода:

  1. resultData...provenance.nodes[].promptVersionId (или сквозная передача в сегменте)
  2. provider_usage_logs.prompt_version_id — LLM-вызов, потребивший токены
  3. 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.