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. Очікування. Завдання проходить processing → done (або 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 }
  • status — processing | done | failed.
  • result_url — заповнено лише при done, інакше null.
  • result_url_2 — другий трек у музичних моделей (Suno), інакше null.
  • results — усі файли завдання: [{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }] (для 3D — модель і прев’ю, для розділення треку — доріжки «Вокал», «Інструментал»…).
  • 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 із причиною ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }), завдання не створюється.

Тіло вебхука ​

Коли завдання переходить у стан 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 }
  • event — media.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 с), таймаут кожної — 15 с. Надішліть відповідь 2xx, інакше буде повтор.
  • Редиректи не виконуються — відповідайте безпосередньо за callback_url.
  • Доставка — best-effort: якщо всі 4 спроби були невдалими (ваш сервер був недоступний), вебхук не надсилається повторно пізніше. Тому:

Вебхуки не замінюють опитування

Вебхук — це прискорення, а не гарантія. Залишайте GET /media/job/:id як запасний шлях: якщо push не надійшов за розумний час — опитайте статус самі. Ідемпотентність на вашому боці: те саме завдання може надійти як вебхук і бути виявлене під час опитування — орієнтуйтеся на 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, ноутбуксервери, продакшн-бекенди

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