Callback и проверка подписи
Если при создании задачи передан
callBackUrl, после завершения мы отправим на негоPOSTс результатом. Подпись подтверждает, что запрос пришёл от нас.
Тело callback
{
"code": 200,
"msg": "Task completed successfully.",
"data": {
"taskId": "3f9c2a7be41d4c0c9a7d5e2b1c8f0a64",
"model": "seedream/5-pro-text-to-image",
"state": "success",
"param": "{\"input\":\"{\\\"prompt\\\":\\\"A red fox sitting in fresh snow, morning light, photorealistic\\\",\\\"aspect_ratio\\\":\\\"1:1\\\",\\\"quality\\\":\\\"basic\\\"}\",\"model\":\"seedream/5-pro-text-to-image\"}",
"resultJson": "{\"resultUrls\":[\"https://files.cr-api.ru/Qm3xV8rTt1ZbW0kq7Lr2aA.png\"]}",
"failCode": null,
"failMsg": null,
"costTime": 14,
"completeTime": 1762941614000,
"createTime": 1762941600000,
"updateTime": 1762941614000,
"creditsConsumed": 5.25
}
}code200 иmsgTask completed successfully.— задача выполнена;code501 иmsgTask failed.— ошибка, причина вdata.failCodeиdata.failMsg.data— тот же объект, что возвращает Статус задачи.
Доставка
- Ответьте любым кодом 2xx в течение 10 секунд — callback считается доставленным. Тело ответа мы не читаем.
- Другой код, таймаут или ошибка соединения — повтор: через 10 секунд, 1, 5 и 15 минут, через час и дальше раз в час в течение 24 часов.
- Доставка «хотя бы один раз»: после сбоя callback может прийти повторно. Отличайте повтор по
data.taskId. - Адрес должен быть публичным: на внутренние и служебные адреса callback не отправляется.
- Редиректы не выполняются.
- Повторно отправить callback можно из кабинета, раздел «Логи».
Подпись
Создайте ключ подписи в кабинете (раздел «Настройки»). С ним каждый callback несёт два заголовка:
| Заголовок | Значение |
|---|---|
X-Webhook-Timestamp | Время отправки, секунды Unix |
X-Webhook-Signature | base64(HMAC-SHA256(taskId + "." + timestamp, ключ подписи)) |
Пока ключа нет, callback уходит без подписи. Проверяйте подпись и время: запрос старше 5 минут отклоняйте.
Проверка
import { createHmac, timingSafeEqual } from "node:crypto";
// Ключ подписи callback из кабинета (раздел «Настройки»).
const WEBHOOK_KEY = "YOUR_WEBHOOK_KEY";
/** body — разобранный JSON запроса, headers — его заголовки (имена в нижнем регистре). */
export function verifyCallback(body, headers) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"];
if (!timestamp || !signature) return false;
// Подпись старше 5 минут — повтор чужого запроса.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", WEBHOOK_KEY)
.update(`${body.data.taskId}.${timestamp}`)
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
import base64
import hashlib
import hmac
import time
# Ключ подписи callback из кабинета (раздел «Настройки»).
WEBHOOK_KEY = "YOUR_WEBHOOK_KEY"
def verify_callback(body: dict, headers: dict) -> bool:
"""body — разобранный JSON запроса, headers — его заголовки (имена в нижнем регистре)."""
timestamp = headers.get("x-webhook-timestamp")
signature = headers.get("x-webhook-signature")
if not timestamp or not signature:
return False
# Подпись старше 5 минут — повтор чужого запроса.
if abs(time.time() - int(timestamp)) > 300:
return False
message = f"{body['data']['taskId']}.{timestamp}".encode()
expected = base64.b64encode(hmac.new(WEBHOOK_KEY.encode(), message, hashlib.sha256).digest()).decode()
return hmac.compare_digest(expected, signature)
Подписаны taskId и время, а не тело целиком. Надёжнее всего после callback прочитать задачу через Статус задачи и брать результат оттуда.