Встраивание в ваше приложение

Вебхуки с результатом

Вебхуки сокращают время между завершением задачи и моментом, когда о нём узнаёт ваше приложение. Это оптимизация доставки: три попытки, после чего шлюз прекращает их. Источником истины остаётся эндпоинт статуса.

#Регистрируйте обратный вызов при отправке

Передайте webhook_url в запросе на отправку. Добавьте webhook_secret, и каждая доставка будет подписана, — без него подписи не будет.

submit.sh
curl https://www.aisplice.store/v1/videos \
  -H "Authorization: Bearer sk-YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: video-job-8842" \
  -d '{
    "model": "doubao-seedance-2.5",
    "prompt": "A cinematic product scene",
    "webhook_url": "https://your.app/hooks/ai-tasks",
    "webhook_secret": "whsec_replace_me"
  }'

Адрес должен быть публичным

localhost, 127.0.0.1, частные диапазоны, link-local-адреса и адреса облачных метаданных отклоняются при отправке задачи, а не при доставке. Во время разработки используйте туннель.

http или https, имя разрешается в момент отправки

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

Доставка не гарантируется

Три повторные попытки, затем шлюз прекращает доставку и только пишет в лог. Источником истины остаётся эндпоинт статуса.

localhost отклоняется ещё при отправке задачи

Хост обратного вызова разрешается и проверяется до приёма задачи, поэтому частный или loopback-адрес приводит к ошибке уже при отправке, а не при доставке через час. Во время разработки направьте webhook_url на публичный туннель или не указывайте его и используйте опрос.

#Заголовки и тело

ЗаголовокКогда передаётсяЧто с ним делать
X-Xiaoxiang-SignatureТолько если задача отправлена с webhook_secret.sha256=<hex HMAC-SHA256 от исходного тела>. Проверяйте до разбора, сравнением за постоянное время.
X-Xiaoxiang-EventПри каждой доставке.task.completed или task.failed — ориентируйтесь на это значение, а не определяйте тип события по содержимому.
X-Xiaoxiang-Event-IdПри каждой доставке.Не меняется между повторными доставками одного события. Сохраняйте его и отбрасывайте повторы — это ваш ключ дедупликации.
ПолеТипКогда естьОписание
event_idstringВсегдаСовпадает с X-Xiaoxiang-Event-Id. Дедуплицируйте по нему.
eventstringВсегда"task.completed" или "task.failed".
task_idstringВсегдаID, возвращённый при отправке задачи, — ваш ключ для сопоставления.
statusstringВсегдаКонечный статус в терминах видео — "completed" или "failed", даже для задач с изображениями и музыкой.
modelstringЕсли известноМодель, выполнившая задачу. Отсутствует, если неизвестна.
urlstringПри успехеURL результата, если задача выполнена успешно. Для стабильной ссылки платформы лучше использовать /content.
fail_reasonstringПри ошибкеТолько при ошибке.
quotaintegerВсегдаФактически израсходованная квота во внутренних единицах. Для сверки используйте журнал использования, а не это поле.
created_atintegerВсегдаВремя отправки задачи, Unix-секунды.
finished_atintegerВсегдаВремя перехода в конечное состояние, Unix-секунды.
delivery.json
{
  "event_id": "task_8f21c...:task.completed",
  "event": "task.completed",
  "task_id": "8f21c0d2-...",
  "status": "completed",
  "model": "doubao-seedance-2.5",
  "url": "https://cdn.example.com/renders/8f21c0d2.mp4",
  "quota": 128000,
  "created_at": 1789000000,
  "finished_at": 1789000184
}

#Проверяйте исходное тело запроса

Вычисляйте HMAC-SHA256 по точным полученным байтам до любого разбора JSON — повторная сериализация меняет байты и ломает сравнение. Сравнивайте за постоянное время и отклоняйте при несовпадении.

import crypto from "node:crypto";

// Express: express.raw({ type: "application/json" }) — not express.json().
export function handleTaskWebhook(req, res) {
  const received = req.get("X-Xiaoxiang-Signature") ?? "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body)              // Buffer of the raw body
      .digest("hex");

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // Deduplicate first, acknowledge fast, then do the work off-request.
  if (!seen.add(req.get("X-Xiaoxiang-Event-Id"))) return res.status(200).end();
  queue.push(event);
  res.status(200).end();
}

#Доставка и дедупликация

Отвечайте в течение 15 секунд

Это тайм-аут доставки. Более медленный ответ считается неудачной попыткой, поэтому сначала поставьте обработку в очередь и верните 2xx.

Дедуплицируйте по ID события

X-Xiaoxiang-Event-Id не меняется между повторами. Сохраняйте его и отбрасывайте повторы до изменения бизнес-данных.

Три повтора, затем тишина

Неудачные доставки повторяются через 5 с, 30 с и 2 мин. После этого шлюз только записывает событие в лог — больше ничего не придёт.

#Оставьте опрос для сверки

Доставка вебхуков не гарантируется

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

Проблема не решена?

Пришлите нам ID запроса или текст ошибки, и мы разберёмся.