Skip to content

Zadania i webhooki ​

Generowanie multimediów (wideo, obrazów, muzyki) jest asynchroniczne: tworzysz zadanie (zadanie) i odbierasz wynik, gdy będzie gotowy. Istnieją dwa sposoby sprawdzenia, czy zadanie zostało zakończone: ankieta (odpytywanie) i webhook (push). Poniżej opisano, jak działa zadanie i jak uzyskać wiarygodne wyniki.

Cykl życia zadania ​

POST /media/generate  →  status: processing  ──►  done   (result_url готов)
        │                                     └─►  failed (error, средства возвращены)
        └── (опционально) callback_url → пуш-вебхук на done/failed
  1. Utworzenie. POST /media/generate z modelem i parametrami - zwraca id i link do ankiety:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Oczekiwanie. Zadanie przechodzi processing → done (lub failed). Obrazy - sekundy, wideo i muzyka - od kilkudziesięciu sekund do kilku minut.

  2. Wynik. Ukończone zadanie ma wypełniony result_url (link do nordrouter.com); plik jest przechowywany przez ograniczony czas (retention_days z GET /media/models) - pobierz, aby zapisać.

Format odpowiedzi GET /media/job/:id (i treści webhooka - patrz poniżej) jest taki sam:

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 - wypełnia się tylko done, w przeciwnym razie null.
  • result_url_2 - drugi utwór dla modeli muzycznych (Suno), inaczej null.
  • cost_usd - koszt rzeczywisty (odpisany po fakcie; nieudana generacja nie naliczona - rezerwa zostaje zwrócona).
  • error - tekst błędu dla failed, w przeciwnym razie null.

Metoda 1. Odpytywanie ​

Najłatwiej jest okresowo wysyłać żądanie GET /media/job/:id, aż status zmieni się na done/failed:

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

Ankieta raz na 2-3 sekundy. Ankieta zawsze działa i nie wymaga adresu publicznego - nadaje się do skryptów, laptopów, CI.

Metoda 2. Webhooki (push) ​

Aby uniknąć odpytywania, przekaż callback_url do POST /media/generate, a po zakończeniu zadania wyślemy POST na Twój adres:

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": "любая-ваша-строка"
  }'

Jeżeli zostanie zaakceptowany callback_url, odpowiedź utworzenia będzie zawierała "webhook": "registered". callback_url jest sprawdzany od razu podczas tworzenia zadania: jeśli nie pasuje (nie https, adres prywatny/lokalny) - żądanie zwróci 400 z powodem, zadanie nie zostanie utworzone.

Treść webhooka ​

Kiedy zadanie osiągnie done/failed, Twój callback_url otrzyma POST z tą samą treścią co GET /media/job/:id plus pole 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 (sukces) lub media.failed (błąd).

Weryfikacja podpisu ​

Jeśli ustawisz callback_secret, nagłówek X-NR-Signature zawiera sha256=<hmac> - HMAC-SHA256 z surowej treści żądania, z kluczem = Twój sekret. Sprawdź, czy prośba pochodzi od nas i nie jest fałszywa:

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));
}

Odczytaj HMAC z bajtów dokładnie tak, jak otrzymano do analizy JSON - w przeciwnym razie podpis nie będzie pasował.

Gotowy odbiornik w 10 liniach ​

Działający serwer, który odbiera webhook, sprawdza podpis i odpowiada 2xx. Sekret jest ten sam, który podałeś do 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, иначе будет повтор

Sprawdź lokalnie

callback_url powinien być publiczny https. Aby programować na laptopie należy podbić tunel - np. ngrok http 3000 - i przekazać wydany przez niego adres https jako callback_url.

Dostawa i niezawodność ​

  • Do 4 prób z rosnącą przerwą (≈3 → 6 → 9 s), każda przerwa wynosi 15 s. Odpowiedz 2xx, w przeciwnym razie zostanie powtórzona.
  • Przekierowania nie działają - odpowiedz bezpośrednio na callback_url.
  • Dostawa — dołożenie wszelkich starań: jeśli wszystkie 4 próby zawiodą (twój serwer nie działa), webhook nie zostanie ponownie wysłany później. Dlatego:

Webhooki nie zastępują ankiet

Webhook to przyspieszenie, a nie gwarancja. Zachowaj GET /media/job/:id jako trasę zapasową: jeśli push nie dotrze w rozsądnym czasie, samodzielnie sprawdź status. Idempotencja jest po Twojej stronie: to samo zadanie może zostać wykonane jako webhook i zostać zauważone w ankiecie - skoncentruj się na id.

Wymagania dla callback_url ​

  • Tylko https://.
  • Tylko adres publiczny. Adresy lokalne i prywatne (localhost, *.local, 10.x, 172.16–31.x, 192.168.x, 127.x, 169.254.x, 100.64–127.x, pętla zwrotna IPv6/ULA/link-local) reject to ochrona SSRF. Adres jest dwukrotnie sprawdzany przed każdą wysyłką (zabezpieczenie przed ponownym powiązaniem DNS).

Co wybrać ​

AnkietaWebhook
Publiczny https adresniepotrzebnypotrzebny
Opóźnienie odbiorudo momentu, aż interwał odpytywaniabędzie prawie natychmiastowy
Gwarancjakontrolujeszbest-effort + ankieta jako rezerwa
Gdzie wygodnieskrypty, CI, laptopserwery, backendy produkcyjne

Rekomendacja: na backendzie - webhook + ankieta jako zabezpieczenie; w skrypcie/laptopie - wystarczy ankieta.