Вебхуки о готовности задач
Как получать уведомление о завершении задачи на свой адрес, проверять подпись X-Epic-Signature и не обработать одно событие дважды.
Укажите адрес в задаче
curl https://epicai.ru/v1/tasks \
-H "Authorization: Bearer $EPICAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana-2",
"prompt": "Логотип кофейни, минимализм",
"webhook_url": "https://example.com/hooks/epicai"
}'
Когда задача завершится, epicai отправит на этот адрес запрос POST. Поле webhook_url принимают также POST /v1/images/generations, POST /v1/audio/speech и POST /v1/audio/transcriptions. Адрес проверяется при создании задачи и должен подходить под условия.
- Схема
https://, порт 443 или без порта, без логина и пароля, не длиннее 500 знаков. - Имя узла ведёт на публичный IPv4-адрес. Адреса вроде
localhost, домены.local,.internal,.locи внутренние сети epicai не принимает. Для отладки откройте свой сервер наружу.
Иначе придёт 400 invalid_parameter, где в param указан webhook_url.
Разберите событие
Событий два. task.succeeded приходит, когда задача завершилась успешно, task.failed когда не получилась (в том числе по истечении 100 минут). К этому моменту файлы уже сохранены, а токены рассчитаны, то есть в price.charged стоит итог.
{
"id": "evt_7fa4be5e14351e3dff994aa3_1",
"object": "event",
"type": "task.succeeded",
"created": 1793114629,
"data": {
"id": "task_7fa4be5e14351e3dff994aa3",
"object": "task",
"type": "video",
"model": "kling",
"status": "succeeded",
"price": {"tokens": 75000, "charged": 75000},
"output": [{"type": "video", "url": "https://epicai.ru/storage/api/7fa4be5e14351e3dff994aa3/1.mp4", "mime": "video/mp4", "bytes": 5236392}],
"expires_at": 1793719429
}
}
В data лежит та же задача, что отдаёт GET /v1/tasks/{id}, показаны не все её поля. Что значит каждое поле, видно в разделе Задачи в API. Запрос приходит с заголовками:
| Заголовок | Значение |
|---|---|
Content-Type | application/json |
User-Agent | epicai-webhooks/1.0 |
X-Epic-Event | Тип события, совпадает с type в теле |
X-Epic-Signature | Время и подпись, вид t=1793114629,v1=… |
Проверьте подпись
Без проверки поддельное событие сможет прислать любой, кому известен ваш адрес. Секрет подписи лежит в key.webhook_secret ответа GET /v1/account и выглядит как whsec_…. У каждого ключа он свой, задача подписывается секретом ключа, которым создана. Храните его так же бережно, как сам ключ.
- Возьмите из заголовка
X-Epic-Signatureвремяtи подписьv1. - Соберите строку из
t, точки и тела запроса в тех же байтах, в каких оно пришло, до разбора JSON. - Посчитайте
HMAC-SHA256этой строки. Ключом служит секрет целиком, вместе сwhsec_. - Сравните шестнадцатеричную запись с
v1функцией сравнения за постоянное время. - Отбросьте событие, если
tрасходится с вашим временем больше чем на 5 минут.
import hashlib
import hmac
import time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp, signature = parts["t"], parts["v1"]
age = abs(time.time() - int(timestamp))
except (KeyError, ValueError):
return False
if age > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
const timestamp = Number(parts.t);
if (!timestamp || !parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(parts.t + ".").update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
В Python передайте в body сырое тело (во Flask это request.get_data()), в Node.js буфер, который отдаёт express.raw. Тело, уже разобранное в объект и собранное обратно, даст другую подпись.
Отвечайте быстро и не обрабатывайте дважды
Событие считается доставленным, когда ваш сервер ответил кодом 2xx в течение 10 секунд. Другой код, перенаправление, ошибка сертификата и таймаут — это неудача. Ответьте 200 сразу, а файлы скачивайте в фоне: ссылки из data.output открываются без ключа.
После неудачи epicai повторяет доставку. Всего делается 6 попыток, первая в момент завершения, остальные через 1 минуту, 5 минут, 30 минут, 2 часа и 6 часов после предыдущей. Повторы запускает фоновый обход, поэтому пауза бывает чуть длиннее названной. После шестой неудачи доставка прекращается.
Одно событие может прийти дважды, например когда ваш ответ потерялся по дороге. В поле id события для защиты не годится, у каждой попытки оно своё (последняя цифра — номер попытки). Запоминайте data.id, то есть номер задачи, и пропускайте уже обработанные.
ПримечаниеВебхук ускоряет получение результата, но не заменяет опрос. Если сервер был недоступен все шесть попыток, задача и файлы остаются доступными по
GET /v1/tasks/{id}в течение 7 суток после завершения.
Не нашли ответ?
Посмотрите частые вопросы или напишите в поддержку.