Skip to content

Получение результатов

Каждая генерация в 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_idintegerID задачи из ответа на создание
statusstringsuccess или fail (терминальные состояния)
codeinteger200 при успехе, код ошибки при fail
result_urlsstring[] | nullПостоянные ссылки на результат при success
errorstring | nullТекст ошибки при fail
modelstringПровайдерная модель задачи
timestampintegerUnix-время формирования уведомления

При 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 и храните в конфигурации.

Правила верификации:

  1. Сверьте X-Callback-Timestamp с текущим временем — отклоняйте уведомления старше 5 минут (защита от replay).
  2. Посчитайте HMAC от "{timestamp}.{raw_body}" — именно от сырого тела, без пересериализации JSON.
  3. Сравнивайте в constant-time (функции hmac.compare_digest, crypto.timingSafeEqual, hash_equals).
  4. Только после успешной проверки обрабатывайте 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
}
ПолеОписание
statuswaitingqueuinggeneratingsuccess / 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 + поллинг вместе (рекомендуемый паттерн)

Для продакшна надёжнее комбинировать:

  1. Передавайте X-Callback-Url — результат придёт мгновенно.
  2. Параллельно запускайте редкий страховочный поллинг (раз в 30–60 секунд): если вебхук не дошёл (все 3 попытки упали), поллинг подберёт результат.
  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)