|
Документация
Заказать демоПлатформа
ПлатформаMCPCLIAPI
Процессы
РуководстваЖурнал изменений

Добро пожаловать

  • Обзор
  • Аутентификация
  • Ошибки и коды статуса
  • Подписи webhook

Локализация

  • Обзор
  • Создать задачи
  • Заблокировать непереводимые ключи
  • Отслеживание группы заданий
  • Получить одно задание
  • Список заданий
  • Доставка через вебхук
  • Прогресс в реальном времени (WebSocket)

Пайплайн

  • Обзор
  • AI-редактирование перед локализацией
  • Проверка человеком
  • AI-оценка (post-edit)
  • Перефразирование для естественного звучания
  • Проверка обратным переводом
  • Настройка пайплайна
  • Как отслеживать запуски пайплайна

Развёртывание

  • Обзор
  • Создать задание развёртывания
  • Типы источников
  • Что извлекает AI
  • Доставка webhook
  • Прогресс в реальном времени (WebSocket)

Синхронный

  • Локализация
  • Распознавание

Управление движком

  • Предложения для движка

Подписи webhook

Когда асинхронная задача завершается, Lingo.dev не заставляет вас постоянно опрашивать её статус. Сервис сам вызывает ваш обработчик: отправляет POST на HTTPS-эндпоинт, который вы зарегистрировали как свой callbackUrl. В этом и удобство. Но в этом же и риск: публичный URL принимает всё, что ему присылают из интернета, и любой, кто узнает ваш адрес, может POST поддельное событие "job completed" в ваш обработчик.

Поэтому правило для любого callback одно: сначала проверяйте, потом доверяйте. Каждая доставка содержит подпись, вычисленную с помощью секрета, который есть только у вас и Lingo.dev. Пересчитайте её на своей стороне, сравните за постоянное время — и поддельный payload не доберётся до вашей бизнес-логики. На этой странице собрано всё об этом механизме. И callbacks локализации, и callbacks развёртывания используют его без изменений — на тех страницах описаны только их собственные форматы payload и даны ссылки сюда для проверки.

На этой странице

  • Три заголовка
  • Секрет подписи
  • Проверка подписи
  • Почему важно исходное тело запроса
  • Защита от повторного воспроизведения
  • Отвечайте быстро, обрабатывайте позже
  • Повторные попытки и backoff

Три заголовка#

Lingo.dev следует спецификации Standard Webhooks — это открытая схема, которую поддерживают несколько провайдеров. Поэтому вы проверяете подпись по опубликованному контракту, а не по чьей-то уникальной вендорской реализации. Каждая доставка включает три заголовка:

ЗаголовокОписание
webhook-idУникальный идентификатор доставки.
webhook-timestampUnix-временная метка в секундах, когда доставка была отправлена.
webhook-signatureСама подпись: v1,{base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))}

Подписываемое содержимое — это три части, соединённые точками: сначала webhook-id, затем webhook-timestamp, затем исходное тело запроса — строго в таком порядке. Соберите эту строку, вычислите для неё HMAC-SHA256 с вашим секретом, закодируйте результат в base64 — и получите значение, которое нужно сравнить.

Заголовок webhook-signature может содержать несколько подписей, разделённых пробелами, и каждая помечена версией схемы (v1,...). Проверка принимает доставку, если совпадает хотя бы одна подпись. Поэтому безопаснее перебирать весь список, а не читать одно значение — именно так и устроены примеры ниже.

Секрет подписи#

Секрет создаётся для вашей организации при первой отправке задачи с callbackUrl. Он начинается с префикса whsec_, за которым идут байты ключа, закодированные в base64:

text
whsec_Mf9aQ7n...base64...key...bytes

Уберите префикс whsec_ и декодируйте оставшуюся часть из base64, чтобы получить исходные байты ключа — именно это декодированное значение и является ключом HMAC, а не строка с префиксом. Подписывать буквальный текст whsec_... — самая частая причина, по которой реализация выглядит правильной, но проверка так и не проходит. Поэтому сначала декодируйте.

Обращайтесь с секретом как с API-ключом

Секрет подписи — это то, что отличает настоящий callback от поддельного. Храните его на стороне сервера, вне системы контроля версий и вне любых клиентских бандлов. Любой, у кого он есть, может подписывать payload, которые ваш обработчик примет. См. API Keys, чтобы узнать, как Lingo.dev работает с учётными данными уровня организации.

Проверка подписи#

Проверка — это одна функция, которую вы один раз ставите перед своим обработчиком. Она делает три вещи: пересчитывает ожидаемую подпись по исходному телу запроса, сравнивает её с пришедшей с помощью проверки за постоянное время и отклоняет всё, что не совпадает, ещё до выполнения вашего кода. Одна и та же функция защищает все асинхронные события, которые отправляет Lingo.dev: завершения локализации, завершения развёртывания — любой тип, в любом продукте.

javascript
import crypto from "node:crypto";

function verifyWebhook(payload, headers, secret) {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];

  // Reject timestamps outside a tolerance window (replay prevention)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    throw new Error("Webhook timestamp too old");
  }

  // Recompute the expected signature over id.timestamp.body
  const content = `${msgId}.${timestamp}.${payload}`;
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");
  const expected = crypto
    .createHmac("sha256", secretBytes)
    .update(content)
    .digest("base64");

  // A delivery may carry several signatures; accept if any matches
  for (const sig of signatures.split(" ")) {
    const [version, value] = sig.split(",", 2);
    if (version === "v1" && crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(value)
    )) {
      return JSON.parse(payload);
    }
  }

  throw new Error("Invalid webhook signature");
}

Сравнивайте с помощью функции постоянного времени — crypto.timingSafeEqual, hmac.compare_digest — а не ==. Обычное сравнение строк возвращает результат сразу, как только находит различие в байтах, и этой разницы по времени достаточно, чтобы постепенно раскрыть подпись байт за байтом. Сравнение за постоянное время закрывает этот побочный канал — поэтому в обоих примерах выше используется именно оно.

Почему важно исходное тело запроса#

Обратите внимание: обе функции подписывают payload — тело ровно в том виде, в каком оно пришло по сети, до любого JSON-парсинга. Именно эта деталь чаще всего ломает даже в остальном корректную интеграцию, и здесь важно сказать об этом прямо:

Подпись вычисляется по точным байтам, которые отправил Lingo.dev. Как только вы парсите тело в объект и сериализуете его заново, могут измениться пробелы, порядок ключей или формат чисел — и пересчитанный HMAC уже не совпадёт с подписью, вычисленной по исходным байтам. По смыслу payload тот же, но байты — уже нет.

Проверяйте по исходному телу, а не по разобранному объекту

Сохраните исходное тело запроса до того, как ваш фреймворк его распарсит, и передайте эти байты в функцию проверки. В Express используйте express.raw({ type: "application/json" }) на маршруте webhook. В FastAPI считывайте await request.body(). Парсить нужно только после успешной проверки подписи — сначала проверка, потом разбор.

Защита от повторного воспроизведения#

Даже корректно подписанный payload, который перехватил злоумышленник, можно дословно отправить повторно — подпись останется действительной, потому что между первой доставкой и копией, отправленной через час, в ней ничего не меняется. Именно заголовок webhook-timestamp ограничивает это окно: он фиксирует время отправки, чтобы ваш код проверки мог отклонять всё, что старше выбранного вами допуска. В примерах выше используется пять минут.

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

Отвечайте быстро, обрабатывайте позже#

Как только доставка проверена, сразу верните 200, а уже потом выполняйте реальную работу — запись в базу данных, вызовы downstream-сервисов, инвалидацию кэша.

javascript
app.post(
  "/webhooks/lingo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    let event;
    try {
      event = verifyWebhook(req.body.toString(), req.headers, process.env.LINGO_WEBHOOK_SECRET);
    } catch {
      return res.status(401).send("invalid signature");
    }

    // Acknowledge first, process after - never block the response on slow work
    res.status(200).send("ok");
    void handleEvent(event);
  }
);

Причина здесь чисто техническая, а не стилистическая. Медленный обработчик держит HTTP-соединение открытым; если он работает достаточно долго, чтобы сработал тайм-аут, доставка считается неуспешной и будет повторена — и тогда тяжёлая работа в пути ответа превращает одно событие в несколько. Быстро подтвердите получение, передайте работу в очередь или фоновую задачу — и одно событие так и останется одним. Форматы payload, по которым вы переключаетесь внутри handleEvent, описаны отдельно для каждого продукта: callbacks локализации и callbacks развёртывания.

Повторные попытки и backoff#

Иногда ваш эндпоинт будет недоступен — из-за деплоя, тайм-аута или ошибки шлюза. В таких случаях Lingo.dev не отбрасывает событие.

Если ваш эндпоинт возвращает статус вне диапазона 2xx или недоступен, доставка повторяется с экспоненциальным backoff, начиная с 30 секунд, до 5 попыток. После пятой попытки доставка помечается как неуспешная, и Lingo.dev прекращает попытки — но результат не теряется. Он остаётся доступным в записи задачи, поэтому даже длительный простой лишит вас callback, но не самого результата. Эта запись задачи — ваша страховка: стройте webhook под типовой сценарий, а сохранённую задачу считайте источником истины, к которому всегда можно вернуться. Для задачи перевода опрашивайте её напрямую.

Следующие шаги#

Аутентификация
Как API-ключи аутентифицируют каждый запрос к API
Webhook локализации
Форматы payload для translation.completed и translation.failed
Webhook развёртывания
Payload callback для задач развёртывания AI движок

Эта страница была полезной?

Max PrilutskiyMax Prilutskiy·Обновлено 12 дней назад·6 минут чтения