Вебхуки
Получайте уведомление в момент завершения асинхронной задачи генерации — вместо опроса
Обзор
Когда вы отправляете асинхронную задачу генерации, 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-Id | session_id задачи — ваш ключ корреляции и идемпотентности. |
X-AtlasCloud-Webhook-Event | Тип события, например video.task.terminal, image.task.terminal или audio.task.terminal. |
X-AtlasCloud-Webhook-Timestamp | Время попытки доставки в секундах Unix epoch. Покрывается подписью Ed25519. |
X-AtlasCloud-Webhook-Signature | HMAC-SHA256 от сырого тела запроса в hex-кодировке (схема HMAC). В чистой схеме Ed25519 здесь передаётся подпись Ed25519 — см. Проверка подписей. |
X-AtlasCloud-Webhook-Signature-Ed25519 | Подпись Ed25519 для <timestamp>.<raw_body> в base64url (отправляется во время миграции с HMAC на Ed25519). |
X-AtlasCloud-Webhook-Key-Id | kid ключа подписи Ed25519 — соответствует ключу в JWKS. |
User-Agent | AtlasCloud-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, которым подписана доставка.
Шаги
- Считайте
X-AtlasCloud-Webhook-Timestamp(ts),X-AtlasCloud-Webhook-Key-Id(kid) и заголовок с подписью Ed25519. - (Рекомендуется) Отклоните запрос, если метка времени расходится с вашими часами более чем на ~5 минут (защита от повторов).
- Загрузите JWKS и выберите ключ с совпадающим
kid. Кэшируйте JWKS; при неизвестномkidвыполните повторную загрузку один раз (ключ подписи мог быть ротирован). - Декодируйте поле JWK
x(base64url) → 32-байтовый публичный ключ Ed25519. - Проверьте декодированную из 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