Вебхуки

Получайте уведомление в момент завершения асинхронной задачи генерации — вместо опроса

Обзор

Когда вы отправляете асинхронную задачу генерации, Atlas Cloud обрабатывает её в фоне, и результат становится доступен некоторое время спустя. Вместо того чтобы многократно обращаться к эндпоинту Предсказания до завершения задачи, вы можете попросить Atlas Cloud вызвать ваш эндпоинт в момент, когда задача достигнет терминального состояния.

Для этого укажите webhook_url при отправке задачи. Когда задача завершится — успешно, с ошибкой или по таймауту — Atlas Cloud отправит на этот URL один подписанный запрос POST с итоговым результатом.

Поддерживаемые типы задач

Вебхуки доступны для асинхронной генерации видео, изображений и аудио. Механизм доставки не зависит от типа задачи — модальность определяет event_type (и заголовок X-AtlasCloud-Webhook-Event): video.task.terminal, image.task.terminal или audio.task.terminal.

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

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

Добавьте поле webhook_url в свой существующий запрос на отправку задачи:

curl -X POST https://api.atlascloud.ai/api/v1/model/generateVideo \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "bytedance/seedance-2.0/text-to-video",
        "prompt": "A calico kitten chasing a butterfly in a garden, cinematic",
        "duration": 5,
        "resolution": "1080p",
        "webhook_url": "https://your-app.example.com/hooks/atlascloud"
      }'

То же поле webhook_url работает одинаково для POST /api/v1/model/generateImage (асинхронные изображения, event_type: "image.task.terminal") и POST /api/v1/model/generateAudio (асинхронное аудио, event_type: "audio.task.terminal").

Ответ на отправку задачи не меняется — вы по-прежнему сразу получаете id задачи (это session_id). Значение webhook_url используется самим Atlas Cloud и никогда не передаётся вышестоящему провайдеру модели. Когда задача завершится, ваш эндпоинт получит запрос POST.

Требования к webhook_url

Ваш URL обратного вызова проверяется в момент отправки задачи. Если проверка не пройдена, запрос на отправку отклоняется с HTTP 400 и задача не создаётся (списания не происходит).

ПравилоДетали
СхемаДолжна быть https://. Обычный http:// отклоняется.
ХостДолжен быть маршрутизируемым публичным адресом. Приватные, loopback-, link-local- и CGNAT-диапазоны (100.64.0.0/10) отклоняются.
ДлинаМаксимум 1024 символа.
ДоступностьДолжен быть доступен из публичного интернета, чтобы Atlas Cloud мог отправить на него POST.

Эти проверки защищают от SSRF. Для локальной разработки используйте публичный туннель (сервис для тестирования вебхуков, ngrok или туннель Cloudflare), а не приватный адрес.

Запрос обратного вызова

Когда задача достигает терминального состояния, Atlas Cloud отправляет POST с Content-Type: application/json и следующими заголовками:

ЗаголовокОписание
X-AtlasCloud-Webhook-Idsession_id задачи — ваш ключ корреляции и идемпотентности.
X-AtlasCloud-Webhook-EventТип события, например video.task.terminal, image.task.terminal или audio.task.terminal.
X-AtlasCloud-Webhook-TimestampВремя попытки доставки в секундах Unix epoch. Покрывается подписью Ed25519.
X-AtlasCloud-Webhook-SignatureHMAC-SHA256 от сырого тела запроса в hex-кодировке (схема HMAC). В чистой схеме Ed25519 здесь передаётся подпись Ed25519 — см. Проверка подписей.
X-AtlasCloud-Webhook-Signature-Ed25519Подпись Ed25519 для <timestamp>.<raw_body> в base64url (отправляется во время миграции с HMAC на Ed25519).
X-AtlasCloud-Webhook-Key-Idkid ключа подписи Ed25519 — соответствует ключу в JWKS.
User-AgentAtlasCloud-Webhook/1.0

Полезная нагрузка

{
  "session_id": "string",       // идентификатор задачи; ваш ключ идемпотентности
  "event_type": "string",       // например "video.task.terminal"
  "status": "OK" | "ERROR",     // общий итог — ветвитесь по этому полю
  "created_at": 1782295062952,  // время создания задачи (мс, epoch)
  "payload": {                  // результат в том же формате, что и в Predictions API
    "model": "string",
    "status": "completed" | "failed" | "timeout",
    "outputs": ["https://..."], // присутствует при успехе
    "error_code": 0             // присутствует при ошибке
  },
  "error": "string"             // присутствует только когда status == "ERROR"
}

Ветвите обработчик по полю верхнего уровня status: OK означает, что пригодный результат находится в payload.outputs; ERROR означает, что задача не дала результата, а поле error объясняет причину.

Результаты транскрибации аудио

Задачи синтеза речи и другие задачи генеративного аудио возвращают URL своих аудиофайлов в payload.outputs — ровно так же, как видео и изображения. Для моделей распознавания речи (транскрибации) завершённый payload дополнительно содержит структурированный объект stt_result (полный текст, определённый язык и временные метки по словам) — то же поле, которое возвращает эндпоинт Предсказания.

Пример успешной доставки

Реальная доставка для завершённой задачи bytedance/seedance-2.0/text-to-video:

{
  "session_id": "6a0c02cdb4b147b7bc78881eb7229ece",
  "event_type": "video.task.terminal",
  "status": "OK",
  "created_at": 1782295062952,
  "payload": {
    "model": "bytedance/seedance-2.0/text-to-video",
    "status": "completed",
    "outputs": [
      "https://atlas-media.oss-us-west-1.aliyuncs.com/videos/cgt-20260624-0.mp4"
    ]
  }
}

Пример ошибки

{
  "session_id": "9b2f4e7a1c0d4f5e8a6b3c2d1e0f9a8b",
  "event_type": "video.task.terminal",
  "status": "ERROR",
  "created_at": 1782200000000,
  "payload": {
    "model": "bytedance/seedance-2.0/text-to-video",
    "status": "failed",
    "error_code": 1039
  },
  "error": "the input was rejected by content moderation"
}

Результат timeout выглядит так же: payload.status: "timeout" и общее сообщение в error.

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

Всегда проверяйте подпись, прежде чем доверять вебхуку. Она подтверждает, что запрос пришёл от Atlas Cloud и не был изменён.

Две схемы на время миграции

Atlas Cloud переходит с подписи вебхуков общим секретом HMAC на Ed25519 с публичным эндпоинтом JWKS. На время перехода доставки содержат обе подписи: HMAC (X-AtlasCloud-Webhook-Signature) и Ed25519 (X-AtlasCloud-Webhook-Signature-Ed25519). Предпочитайте Ed25519 — проверка выполняется по публичному ключу, который вы загружаете по URL, и хранить общий секрет не нужно.

Ed25519 + JWKS (рекомендуется)

Atlas Cloud подписывает каждую доставку приватным ключом Ed25519 и публикует соответствующий публичный ключ на эндпоинте JWKS. Вы проверяете подпись по этому публичному ключу — на вашей стороне нет секрета, который нужно выдавать или ротировать.

  • Публичные ключи (JWKS): GET https://api.atlascloud.ai/api/v1/webhooks/jwks.json (без аутентификации):
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "Wn_rgZDBO6nv4-ka97PPf5z8WXbM25o75dR6YukTqqI",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "cnut8IN2blKoB5zRJr9th0HoLzH-iBH3WYoUtkcJZQE"
    }
  ]
}
  • Подписываемое сообщение: "<timestamp>.<raw_body>" — значение X-AtlasCloud-Webhook-Timestamp, буквальная точка ., затем точное сырое тело запроса. В отличие от HMAC, метка времени входит в подпись, поэтому вы можете задать окно защиты от повторов.
  • Заголовок с подписью: X-AtlasCloud-Webhook-Signature-Ed25519 (base64url). После отказа от HMAC подпись Ed25519 переедет в X-AtlasCloud-Webhook-Signature — поэтому используйте заголовок -Ed25519, когда он есть, и откатывайтесь на -Signature.
  • Идентификатор ключа: X-AtlasCloud-Webhook-Key-Id — это kid того JWK, которым подписана доставка.

Шаги

  1. Считайте X-AtlasCloud-Webhook-Timestamp (ts), X-AtlasCloud-Webhook-Key-Id (kid) и заголовок с подписью Ed25519.
  2. (Рекомендуется) Отклоните запрос, если метка времени расходится с вашими часами более чем на ~5 минут (защита от повторов).
  3. Загрузите JWKS и выберите ключ с совпадающим kid. Кэшируйте JWKS; при неизвестном kid выполните повторную загрузку один раз (ключ подписи мог быть ротирован).
  4. Декодируйте поле JWK x (base64url) → 32-байтовый публичный ключ Ed25519.
  5. Проверьте декодированную из base64url подпись для ts + "." + raw_body.
const crypto = require("crypto");

const JWKS_URL = "https://api.atlascloud.ai/api/v1/webhooks/jwks.json";
let jwks = {}; // kid -> jwk

async function publicKey(kid) {
  if (!jwks[kid]) {
    const { keys } = await (await fetch(JWKS_URL)).json();
    jwks = Object.fromEntries(keys.map((k) => [k.kid, k]));
  }
  const jwk = jwks[kid];
  return jwk && crypto.createPublicKey({ key: jwk, format: "jwk" });
}

// req.body должен быть СЫРЫМ телом в виде Buffer (например, express.raw()).
async function verifyEd25519(req) {
  const ts = req.get("X-AtlasCloud-Webhook-Timestamp");
  const kid = req.get("X-AtlasCloud-Webhook-Key-Id");
  const sig =
    req.get("X-AtlasCloud-Webhook-Signature-Ed25519") ||
    req.get("X-AtlasCloud-Webhook-Signature");

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // окно защиты от повторов

  const pub = await publicKey(kid);
  if (!pub) return false;
  const msg = Buffer.concat([Buffer.from(`${ts}.`), req.body]);
  return crypto.verify(null, msg, pub, Buffer.from(sig, "base64url"));
}

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

HMAC (устаревшая схема)

Подпись HMAC признана устаревшей в пользу Ed25519/JWKS, описанной выше; новым интеграциям следует использовать Ed25519. Заголовок HMAC X-AtlasCloud-Webhook-Signature продолжает отправляться в течение периода миграции.

Подпись вычисляется так:

HMAC-SHA256( signing_secret, raw_request_body )   →   lowercase hex
  • Ключ — это ваш общий секрет подписи (выдаётся для вашего аккаунта).
  • Сообщение — точные сырые байты тела запроса: проверяйте подпись до любой повторной сериализации JSON, которая может изменить порядок байтов или пробелы.
  • Сравнивайте результат со значением заголовка X-AtlasCloud-Webhook-Signature, используя сравнение за константное время.

Заголовок X-AtlasCloud-Webhook-Timestamp носит информационный характер (полезен для необязательной проверки окна повторов). Он не входит в подписываемое содержимое — подписывается только сырое тело.

const crypto = require("crypto");
const express = require("express");
const app = express();

const SIGNING_SECRET = process.env.ATLASCLOUD_WEBHOOK_SECRET;

// Получите СЫРОЕ тело — не позволяйте JSON-парсеру отработать первым.
app.use("/hooks/atlascloud", express.raw({ type: "*/*" }));

app.post("/hooks/atlascloud", (req, res) => {
  const sig = req.get("X-AtlasCloud-Webhook-Signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.body)               // req.body — это Buffer (сырые байты)
    .digest("hex");

  // Сравнивайте как Buffer и проверяйте длину в БАЙТАХ: timingSafeEqual бросает
  // исключение на входах разной длины, а некорректный многобайтовый заголовок
  // может совпасть по длине строки в JS, отличаясь по длине в байтах.
  const sigBuf = Buffer.from(sig);
  const expBuf = Buffer.from(expected);
  const ok =
    sigBuf.length === expBuf.length &&
    crypto.timingSafeEqual(sigBuf, expBuf);
  if (!ok) return res.status(401).send("invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // ... поставьте в очередь по event.session_id, затем быстро ответьте ...
  res.status(200).send("ok");
});

Семантика доставки

Подтверждение доставки

Ответьте любым кодом состояния 2xx, чтобы подтвердить получение. Любой другой код состояния — или таймаут соединения — считается сбоем, и доставка повторяется.

Отвечайте быстро (с хорошим запасом внутри нескольких секунд). Реальную работу выполняйте асинхронно: проверьте подпись, поставьте событие в очередь с ключом session_id и сразу верните 200. Медленные ответы рискуют завершиться таймаутом и вызвать лишние повторы.

Повторы и экспоненциальная задержка

Если доставка не подтверждена, Atlas Cloud повторяет её с экспоненциально растущей задержкой (примерно 10s → 20s → 40s → …, с ограничением около 30 минут), суммарно до ~10 попыток. После исчерпания попыток доставка помечается недоставляемой и больше не повторяется.

Доставка «как минимум один раз» — дедупликация по session_id

Доставка выполняется как минимум один раз (at-least-once). В редких случаях вы можете получить один и тот же вебхук более одного раза. Сделайте обработчик идемпотентным и дедуплицируйте по session_id.

Значение session_id остаётся неизменным при повторах. Считайте вебхук с уже полностью обработанным session_id пустой операцией и возвращайте 200.

Своевременность

Подавляющее большинство вебхуков доставляется в течение нескольких секунд после завершения задачи. Встроенный механизм сверки гарантирует доставку, даже если быстрый путь был пропущен (например, во время развёртывания сервиса), ценой задержки до ~30 минут в таких редких случаях. Проектируйте под согласованную в конечном счёте доставку как минимум один раз, а не под мгновенную и ровно однократную.

Лучшие практики

  • Публикуйте обратный вызов по HTTPS на публично доступном хосте.
  • Проверяйте подпись вебхука, прежде чем доверять событию — предпочтительно Ed25519/JWKS (секрет хранить не нужно; проверка по публичному ключу, найденному по kid). HMAC — устаревший запасной вариант на период миграции.
  • При проверке Ed25519 кэшируйте JWKS и перезагружайте его при неизвестном kid; проверяйте подпись для "<timestamp>.<raw_body>" и соблюдайте окно защиты от повторов.
  • Быстро отвечайте 2xx; реальную обработку переносите в фоновую очередь.
  • Дедуплицируйте по session_id — обработчики должны быть идемпотентными.
  • Ветвитесь по полю верхнего уровня status (OK или ERROR); результаты читайте из payload.outputs.
  • Не рассчитывайте на порядок доставки или ровно однократную доставку; проектируйте под «как минимум один раз».
  • Оставьте эндпоинт Предсказания как запасной путь и способ сверки.
  • Только для устаревшего HMAC: храните секрет подписи в тайне и ротируйте его при утечке. (У Ed25519/JWKS секрета на вашей стороне нет.)

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

СимптомВероятная причина / действие
Отправка возвращает 400 webhook_url ... is not a routable public addressХост приватный/loopback/CGNAT. Используйте публичный HTTPS-URL или туннель.
Отправка возвращает 400 webhook_url must use httpsСмените схему на https://.
Отправка возвращает 400 webhook_url exceeds the 1024-character limitУкоротите URL (перенесите состояние в собственное хранилище с ключом session_id).
Вебхук не приходитУбедитесь, что эндпоинт публично доступен и возвращает 2xx; проверьте через эндпоинт Предсказания, что задача действительно достигла терминального состояния.
Подпись не совпадает (Ed25519)Подписывайте "<timestamp>.<raw_body>" (а не одно только тело), декодируйте подпись из base64url и ищите ключ по kid в JWKS.
Подпись не совпадает (HMAC)Убедитесь, что вычисляете HMAC по сырым байтам тела (а не по повторно сериализованному объекту JSON) и используете правильный секрет подписи.
kid отсутствует в JWKSКлюч подписи был ротирован — перезагрузите JWKS (не кэшируйте один ключ навсегда).
Одно и то же событие получено дваждыОжидаемо при доставке «как минимум один раз» — дедуплицируйте по session_id.

Справочник

  • Отправка задачи (с вебхуком): POST /api/v1/model/generateVideo, POST /api/v1/model/generateImage или POST /api/v1/model/generateAudio — добавьте webhook_url.
  • Типы событий: video.task.terminal, image.task.terminal, audio.task.terminal.
  • JWKS (публичные ключи): GET /api/v1/webhooks/jwks.json.
  • Подпись (Ed25519, рекомендуется): Ed25519 в base64url для "<timestamp>.<raw_body>", в заголовке X-AtlasCloud-Webhook-Signature-Ed25519 (идентификатор ключа — в X-AtlasCloud-Webhook-Key-Id).
  • Подпись (HMAC, устаревшая): HMAC-SHA256(signing_secret, raw_body), hex, в X-AtlasCloud-Webhook-Signature.
  • Ключ идемпотентности: session_id (также в X-AtlasCloud-Webhook-Id).
  • Альтернатива с опросом: Предсказания.

Last updated on

On this page