Skip to content

Задачі і веб-хоки

Генерація медіа (від'єки, зображення, музика) асинхронна: ви створюєте задача (задача) і отримуєте результат, коли вона готова. Є два способи дізнатися, що завдання завершено: опитування (опитування) і веб-бук (поштовх). Нижче як організовано завдання і як отримати надійний результат.

Життєвий цикл задач

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. Опитування (опитування)

Найпростіший спосіб - періодично запитувати GET /media/job/:id, поки status не стане done/failed:

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

Запитання працює завжди і не вимагає публічного адресу, підходить для скриптів, ноутбуків, CI.

Метод 2. Вебхуки (пуш)

Щоб не задати запит, передайте 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 }
  • 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 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.

Що вибрати?

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

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