Skip to content

Задачи и вебхуки

Генерация медиа (видео, изображения, музыка) — асинхронная: вы создаёте задачу (job) и забираете результат, когда он готов. Есть два способа узнать, что задача завершилась: опрос (polling) и вебхук (push). Ниже — как устроена задача и как получать результат надёжно.

Жизненный цикл задачи

POST /media/generate  →  status: processing  ──►  done   (result_url готов)
        │                                     └─►  failed (error, средства возвращены)
        └── (опционально) callback_url → пуш-вебхук на done/failed
  1. Создание. POST /media/generate с моделью и параметрами — возвращает id и ссылку для опроса:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Ожидание. Задача проходит processingdone (или failed). Изображения — секунды, видео и музыка — от десятков секунд до пары минут.

  2. Результат. У завершённой задачи заполнен result_url (ссылка на nordrouter.com); файл хранится ограниченное время (retention_days из GET /media/models) — скачайте, чтобы сохранить.

Формат ответа GET /media/job/:id (и тела вебхука — см. ниже) одинаковый:

json
{ "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • statusprocessing | done | failed.
  • result_url — заполнен только при done, иначе null.
  • result_url_2 — второй трек у музыкальных моделей (Suno), иначе null.
  • cost_usd — фактическая стоимость (списывается по факту; неудачные генерации не тарифицируются — резерв возвращается).
  • error — текст ошибки при failed, иначе null.

Способ 1. Опрос (polling)

Самый простой путь — периодически запрашивать GET /media/job/:id, пока status не станет done/failed:

bash
curl https://nordrouter.com/media/job/beefb531-…

Опрашивайте раз в 2–3 секунды. Опрос работает всегда и не требует публичного адреса — подходит для скриптов, ноутбуков, CI.

Способ 2. Вебхуки (push)

Чтобы не опрашивать — передайте callback_url в POST /media/generate, и мы сами отправим POST на ваш адрес, когда задача завершится:

bash
curl https://nordrouter.com/media/generate \
  -H "Authorization: Bearer sk-nr-YOUR-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "image/nano-banana-2",
    "input": { "prompt": "…" },
    "callback_url": "https://your-server.com/webhooks/nordrouter",
    "callback_secret": "любая-ваша-строка"
  }'

Если callback_url принят, ответ на создание содержит "webhook": "registered". callback_url проверяется сразу при создании задачи: если он не подходит (не https, приватный/локальный адрес) — запрос вернёт 400 с причиной, задача не создаётся.

Тело вебхука

Когда задача доходит до done/failed, на ваш callback_url приходит POST с тем же телом, что и GET /media/job/:id, плюс поле event:

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • eventmedia.completed (успех) или media.failed (ошибка).

Проверка подписи

Если вы задали callback_secret, заголовок X-NR-Signature содержит sha256=<hmac> — HMAC-SHA256 от сырого тела запроса, с ключом = ваш секрет. Проверьте его, чтобы убедиться, что запрос от нас, а не подделка:

python
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)  # сравнение за константное время
javascript
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Считайте HMAC от байтов ровно как пришли, до JSON-парсинга — иначе подпись не сойдётся.

Готовый приёмник за 10 строк

Рабочий сервер, который принимает вебхук, проверяет подпись и отвечает 2xx. Секрет — тот же, что вы передали в callback_secret.

Node.js / Express:

javascript
import express from "express";
import crypto from "node:crypto";

const SECRET = "любая-ваша-строка";           // = ваш callback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // сырое тело — обязательно для подписи
  (req, res) => {
    const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const got = req.headers["x-nr-signature"] || "";
    if (expected.length !== got.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got)))
      return res.sendStatus(401);             // подпись не сошлась — не наш запрос
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("готово:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // ответьте 2xx, иначе будет повтор
  });

app.listen(3000);

Python / FastAPI:

python
import hmac, hashlib
from fastapi import FastAPI, Request, Response

SECRET = b"любая-ваша-строка"                  # = ваш callback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # сырые байты — обязательно для подписи
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-NR-Signature", "")):
        return Response(status_code=401)       # подпись не сошлась — не наш запрос
    job = await request.json()
    if job["event"] == "media.completed":
        print("готово:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # ответьте 2xx, иначе будет повтор

Проверить локально

callback_url должен быть публичным https. Для разработки на ноутбуке поднимите туннель — например ngrok http 3000 — и передайте выданный им https-адрес как callback_url.

Доставка и надёжность

  • До 4 попыток с нарастающей паузой (≈3 → 6 → 9 c), таймаут каждой — 15 c. Ответьте 2xx, иначе будет повтор.
  • Редиректы не проходятся — отвечайте напрямую по callback_url.
  • Доставка — best-effort: если все 4 попытки не удались (ваш сервер лежал), вебхук не переотправляется позже. Поэтому:

Вебхуки не заменяют опрос

Вебхук — это ускорение, а не гарантия. Держите GET /media/job/:id как запасной путь: если пуш не пришёл за разумное время — опросите статус сами. Идемпотентность на вашей стороне: одна и та же задача может прийти как вебхук и быть замечена опросом — ориентируйтесь на id.

Требования к callback_url

  • Только https://.
  • Только публичный адрес. Локальные и приватные адреса (localhost, *.local, 10.x, 172.16–31.x, 192.168.x, 127.x, 169.254.x, 100.64–127.x, IPv6 loopback/ULA/link-local) отклоняются — это защита от SSRF. Адрес перепроверяется и перед каждой отправкой (защита от DNS-rebinding).

Что выбрать

ОпросВебхук
Публичный https-адресне нуженнужен
Задержка получениядо интервала опросапочти мгновенно
Гарантиявы контролируетеbest-effort + опрос как запас
Где удобноскрипты, CI, ноутбуксерверы, продакшн-бэкенды

Рекомендация: на бэкенде — вебхук + опрос как страховка; в скрипте/ноутбуке — просто опрос.