Appearance
Получение результатов
Каждая генерация в API — асинхронная задача. Эндпоинт создания возвращает {"task_id": 123}, а готовый результат вы получаете одним из двух способов (можно комбинировать):
| Callback (push) | Поллинг (pull) | |
|---|---|---|
| Как включить | Заголовок X-Callback-Url при создании | Ничего — опрашивайте GET /task/{task_id} |
| Когда приходит результат | Мы сами отправим POST на ваш сервер | Вы спрашиваете по расписанию |
| Подходит для | Продакшн-интеграций, высоких нагрузок | Простых скриптов, тестов, локальной разработки |
Callback-уведомления
Как передать URL
Добавьте заголовок в любой запрос создания генерации (/images/*, /videos/*, /sounds/*):
X-Callback-Url: https://myapp.example.com/api/aihere-callbackТребования к URL:
- только
http/https, до 500 символов; - публично доступный хост — приватные адреса (localhost, 127.0.0.0/8, 10/8, 192.168/16, link-local, metadata) отклоняются на этапе создания задачи, включая проверку DNS-резолва домена (защита от SSRF);
- ваш сервер должен быстро отвечать любым
2xx(таймауты доставки: подключение ~5 с, чтение ~10 с).
Формат уведомления
После завершения задачи (успех или ошибка) мы отправляем POST с JSON:
json
{
"task_id": 123,
"status": "success",
"code": 200,
"result_urls": [
"https://storage.yandexcloud.net/.../api-results/123/0.mp4"
],
"error": null,
"model": "veo3",
"timestamp": 1754300400
}| Поле | Тип | Описание |
|---|---|---|
task_id | integer | ID задачи из ответа на создание |
status | string | success или fail (терминальные состояния) |
code | integer | 200 при успехе, код ошибки при fail |
result_urls | string[] | null | Постоянные ссылки на результат при success |
error | string | null | Текст ошибки при fail |
model | string | Провайдерная модель задачи |
timestamp | integer | Unix-время формирования уведомления |
При
failдля задачи автоматически ставится возврат средств (best-effort). Перед повторной постановкой сверьте баланс через GET /balance.
Заголовки и подпись
X-Callback-Timestamp: 1754300400
X-Callback-Signature: sha256=9f1c2ab7...Подпись считается так:
X-Callback-Signature = "sha256=" + HMAC_SHA256(secret, "{timestamp}." + <сырое тело запроса>)Секрет персональный и постоянный для аккаунта — получите один раз через GET /callback/secret и храните в конфигурации.
Правила верификации:
- Сверьте
X-Callback-Timestampс текущим временем — отклоняйте уведомления старше 5 минут (защита от replay). - Посчитайте HMAC от
"{timestamp}.{raw_body}"— именно от сырого тела, без пересериализации JSON. - Сравнивайте в constant-time (функции
hmac.compare_digest,crypto.timingSafeEqual,hash_equals). - Только после успешной проверки обрабатывайте payload.
Примеры верификации
python
import hashlib, hmac, time
from fastapi import FastAPI, Request, HTTPException
CALLBACK_SECRET = "..." # из GET /callback/secret
app = FastAPI()
@app.post("/api/aihere-callback")
async def aihere_callback(request: Request):
raw = await request.body()
ts = request.headers.get("X-Callback-Timestamp", "")
sig = request.headers.get("X-Callback-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
raise HTTPException(400, "stale timestamp")
expected = "sha256=" + hmac.new(
CALLBACK_SECRET.encode(), f"{ts}.".encode() + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise HTTPException(401, "bad signature")
data = await request.json()
if data["status"] == "success":
save_result(data["task_id"], data["result_urls"])
else:
mark_failed(data["task_id"], data["error"])
return {"ok": True}javascript
const crypto = require("crypto");
const express = require("express");
const CALLBACK_SECRET = "..."; // из GET /callback/secret
const app = express();
// ВАЖНО: нужен сырой body — не express.json()
app.post("/api/aihere-callback", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-Callback-Timestamp") || "";
const sig = req.get("X-Callback-Signature") || "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send("stale timestamp");
}
const expected =
"sha256=" +
crypto.createHmac("sha256", CALLBACK_SECRET)
.update(`${ts}.`)
.update(req.body) // Buffer с сырым телом
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send("bad signature");
}
const data = JSON.parse(req.body.toString());
if (data.status === "success") saveResult(data.task_id, data.result_urls);
else markFailed(data.task_id, data.error);
res.json({ ok: true });
});php
$secret = '...'; // из GET /callback/secret
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CALLBACK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_CALLBACK_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
http_response_code(400);
exit('stale timestamp');
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('bad signature');
}
$data = json_decode($raw, true);
if ($data['status'] === 'success') {
saveResult($data['task_id'], $data['result_urls']);
} else {
markFailed($data['task_id'], $data['error']);
}
echo json_encode(['ok' => true]);go
func aihereCallback(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
ts := r.Header.Get("X-Callback-Timestamp")
sig := r.Header.Get("X-Callback-Signature")
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-tsInt)) > 300 {
http.Error(w, "stale timestamp", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(callbackSecret))
mac.Write([]byte(ts + "."))
mac.Write(raw)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(sig)) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
// ... обработка payload
w.WriteHeader(http.StatusOK)
}Ретраи доставки
Если ваш сервер не ответил 2xx:
- всего 3 попытки — сразу, через 5 секунд и через 15 секунд;
- таймауты попытки: подключение ~5 с, чтение ~10 с;
- недоставленный callback не отменяет задачу — результат всегда можно забрать поллингом.
Отвечайте на вебхук быстро (200 сразу), а тяжёлую обработку (скачивание файла, постобработку) выполняйте в фоне.
Поллинг результатов
Эндпоинт
GET /task/{task_id}
Authorization: Bearer <API-ключ>Полное описание: GET /task/{task_id}.
Ответ:
json
{
"status": "generating",
"result_urls": null,
"error": null
}| Поле | Описание |
|---|---|
status | waiting → queuing → generating → success / fail |
result_urls | При success — массив постоянных ссылок, иначе null |
error | При fail — объект {"code": 500, "message": "..."} |
Стратегия поллинга
- опрашивайте не чаще 1 раза в 3 секунды (ограничение эндпоинта);
- терминальные статусы —
successиfail: после них задача не изменится; - при
failдля задачи ставится возврат средств (best-effort); - видео может генерироваться 5–15 минут — используйте backoff и разумный общий таймаут.
Готовый цикл ожидания
python
import time
import requests
BASE = "https://api.aihere.ru/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def wait_result(task_id: int, timeout: float = 900) -> list[str]:
"""Ждёт завершения задачи, возвращает result_urls. Бросает исключение при fail/таймауте."""
deadline = time.monotonic() + timeout
delay = 3.0
while time.monotonic() < deadline:
r = requests.get(f"{BASE}/task/{task_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json()
if data["status"] == "success":
return data["result_urls"]
if data["status"] == "fail":
err = data.get("error") or {}
raise RuntimeError(f"Задача {task_id} failed: {err.get('code')} {err.get('message')}")
time.sleep(delay)
delay = min(delay * 1.5, 15.0) # 3с → 4.5с → … → максимум 15с
raise TimeoutError(f"Задача {task_id} не завершилась за {timeout}с")
urls = wait_result(123)
print("Готово:", urls)javascript
const BASE = "https://api.aihere.ru/api/v1";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitResult(taskId, timeoutMs = 900_000) {
const deadline = Date.now() + timeoutMs;
let delay = 3_000;
while (Date.now() < deadline) {
const resp = await fetch(`${BASE}/task/${taskId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data.status === "success") return data.result_urls;
if (data.status === "fail") {
throw new Error(`Задача ${taskId} failed: ${data.error?.code} ${data.error?.message}`);
}
await sleep(delay);
delay = Math.min(delay * 1.5, 15_000);
}
throw new Error(`Задача ${taskId} не завершилась за отведённое время`);
}Callback + поллинг вместе (рекомендуемый паттерн)
Для продакшна надёжнее комбинировать:
- Передавайте
X-Callback-Url— результат придёт мгновенно. - Параллельно запускайте редкий страховочный поллинг (раз в 30–60 секунд): если вебхук не дошёл (все 3 попытки упали), поллинг подберёт результат.
- Обработку результата делайте идемпотентной по
task_id— задача может прийти и через callback, и через поллинг.
python
# псевдокод обработчика
def on_task_done(task_id, result_urls):
if already_processed(task_id):
return
mark_processed(task_id)
store(task_id, result_urls)