Медиазадача переживает HTTP-вызов, который её создал, страницу, которая её запустила, и нередко тот деплой, что шёл в это время. Всё дальнейшее — про то, как это пережить.
Между отправкой и готовым файлом есть четыре места, где теряется работа: вслепую повторённый вызов создания после таймаута, обратный вызов, который нельзя проверить, цикл опроса, который долбит или сдаётся, и ссылка на результат, истёкшая, пока вы решали, что с ней делать.
01
Повтор без ключа — это второй счёт
Idempotency-Key делает так, что повтор той же модели, того же ввода и того же адреса обратного вызова в течение 24 часов возвращает исходную задачу и не резервирует средства дважды. Область действия — один ключ API; соседний ключ с тем же значением получит 409 и не получит идентификатор задачи.
02
Непроверенный обратный вызов — это открытая дверь
Доставки несут X-Webhook-Timestamp и Base64-подпись HMAC-SHA256 в X-Webhook-Signature, вычисленную по taskId.timestamp.sha256(raw_body). Сверяйте по сырым байтам за постоянное время и отклоняйте устаревшие отметки до изменения состояния задачи или списаний.
03
Опрос без потолка — это заявка на ограничение частоты
recordInfo — это резервный путь, а не транспорт. Начните примерно с двух секунд, увеличивайте в полтора раза, остановитесь на пятнадцати и завершайте по сроку, который выбрали вы, а не сеть.
Подход
Одна долговечная запись на задачу, записанная до ответа
Сохраняйте ключ идемпотентности, taskId и собственный идентификатор корреляции в той же транзакции, в которой отвечаете вызывающей стороне. Любое последующее событие — вызов, опрос, повтор, вопрос поддержки — разрешается в эту строку, и повторная доставка становится обновлением, а не вторым заказом.
Сначала проверка, потом чтение. Подпись покрывает сырые байты, поэтому разбор идёт после сравнения.
Verify the callback before you trust it
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the exact bytes that arrived. Parse only to read the task ID:
// the digest is always computed over those raw bytes, never over a re-encode.
export function verifyDelivery(rawBody, headers, webhookKey) {
const timestamp = headers["x-webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const taskId = JSON.parse(rawBody).data.taskId; // payload version 2
const digest = createHash("sha256").update(rawBody).digest("hex");
const expected = createHmac("sha256", webhookKey)
.update(`${taskId}.${timestamp}.${digest}`)
.digest("base64");
const sent = Buffer.from(headers["x-webhook-signature"] ?? "", "utf8");
const mine = Buffer.from(expected, "utf8");
return sent.length === mine.length && timingSafeEqual(sent, mine);
}
Асинхронное медиа-API: задачи, вебхуки, повторы — Что стоит зафиксировать сразу
Поле или заголовок
Значение или предел
Idempotency-Key
24 h · scoped to one API key
X-Webhook-Signature
base64(HMAC-SHA256)
signed string
taskId.timestamp.sha256(raw_body)
X-Webhook-Payload-Version
2 for new integrations
Проведите по горизонтали, чтобы увидеть все столбцы.
Интеграция
От первого вызова до продакшена
01Создайте и сохраните ключ идемпотентности, затем вызовите createTask с ним и с callBackUrl на HTTPS-адрес по порту 80 или 443.
02Запишите data.taskId в базу до ответа вызывающей стороне: 202 — единственное место, где он появляется первым.
03Примите обратный вызов, проверьте подпись по сырому телу, отсейте дубли по request_id и обновите запись.
04Держите recordInfo с экспоненциальной паузой как резерв и копируйте output.assets до истечения подписанной ссылки.
Применимость
Когда этот путь подходит
Выбирайте, если
Всё, что производит медиа. Эндпоинты изображений, видео и звука асинхронны по устройству.
Нужна долговечная запись о том, что отправлено, сколько это должно было стоить и чем закончилось.
Ищите другое, если
Вы вызываете текстовую модель: она отдаёт поток внутри запроса и рассчитывается по измеренному потреблению.
Вы хотите отдать задачу браузеру. Ключи принадлежат серверу, и запись о задаче тоже.
Проверки
Что вы можете проверить сами
В каждом ответе есть request_id
Конверт у всех операций /api/v1 — {code, msg, data, request_id}. В версии полезной нагрузки 2 request_id доставки не меняется при повторной отправке, поэтому он и есть естественный ключ дедупликации.
Резервы видны в записи
cost при settled: false — это зарезервированная оценка, settled: true — итоговое списание. Успех никогда не превышает резерв, а ошибка или истечение срока освобождают его полностью.
Отказы описаны закрытым словарём
errorCode принимает одно из девяти стабильных значений: invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset, upstream_failed. Любое другое значение трактуйте как upstream_failed.
Перед запуском
Поведение платформы, которое стоит подтвердить
Храните ключ идемпотентности вместе с задачей, а не в памяти процесса.
Сделайте обработчик обратного вызова идемпотентным: доставки повторяются по замыслу.
Задайте срок опроса и оповещайте о задачах, которые его превысили.
Копируйте файлы до expiresAt и запрашивайте ссылку заново, а не кешируйте подписанную.
Частые вопросы
О чём спрашивают на этой странице
Повторите тот же запрос с тем же Idempotency-Key. В течение 24 часов и с тем же ключом API вы получите исходную задачу без второго резерва. Никогда не отправляйте новое тело после таймаута: это новая задача с отдельным списанием.
И то и другое. Обратный вызов — быстрый путь, ограниченный опрос — страховка на случай доставки, которая так и не пришла. Цикл опроса не должен быть основным транспортом.
Вычислите HMAC-SHA256 по taskId.timestamp.sha256(raw_body) с вебхук-ключом аккаунта, закодируйте в Base64 и сравните за постоянное время. В версии 2 идентификатор берётся из data.taskId.
Нет. Приём окончателен. Состояние canceled существует, чтобы старые записи оставались читаемыми, а не как действие, которое можно вызвать.
Модели
Модели, которые можно вызвать для этой задачи
Каждая ссылка ведёт на страницу семейства моделей: входные данные, ограничения, контентная политика и текущая цена за единицу.