Skip to content

Zadania i webhooki ​

Generowanie mediów (wideo, obrazów, muzyki) jest asynchroniczne: tworzysz zadanie (job), a wynik pobierasz, gdy będzie gotowy. Są dwa sposoby sprawdzenia, czy zadanie zostało ukończone: odpytywanie (polling) i webhook (push). Poniżej opisano, jak działa zadanie i jak niezawodnie odbierać wynik.

Cykl życia zadania ​

POST /media/generate  →  status: processing  ──►  done   (result_url gotowy)
        │                                     └─►  failed (error, środki zwrócone)
        └── (opcjonalnie) callback_url → webhook push po done/failed
  1. Utworzenie. POST /media/generate z modelem i parametrami zwraca id oraz adres do odpytywania:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Oczekiwanie. Zadanie przechodzi ze stanu processing do done (lub failed). Generowanie obrazów trwa kilka sekund, a wideo i muzyki — od kilkudziesięciu sekund do kilku minut.

  2. Wynik. W ukończonym zadaniu pole result_url jest wypełnione (link do nordrouter.com); plik jest przechowywany przez ograniczony czas (retention_days z GET /media/models) — pobierz go, aby go zachować.

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łnione tylko przy done, w przeciwnym razie null.
  • result_url_2 — drugi utwór w modelach muzycznych (Suno), w przeciwnym razie null.
  • results — wszystkie pliki zadania: [{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }] (w przypadku 3D — model i podgląd, a przy rozdzielaniu utworu — ścieżki „Wokal”, „Instrumental”…).
  • cost_usd — rzeczywisty koszt (naliczany po wykonaniu; nieudane generacje nie są rozliczane — rezerwa jest zwracana).
  • error — tekst błędu przy failed, w przeciwnym razie null.

Sposób 1. Odpytywanie (polling) ​

Najprostszy sposób to okresowe wysyłanie żądań GET /media/job/:id, aż status zmieni się na done/failed:

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

Wysyłaj zapytanie co 2–3 sekundy. Odpytywanie działa zawsze i nie wymaga publicznego adresu — sprawdza się w skryptach, notebookach i CI.

Sposób 2. Webhooki (push) ​

Aby nie odpytywać serwera, przekaż callback_url w POST /media/generate, a sami wyślemy żądanie POST na ten adres po zakończeniu zadania:

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": "dowolny-twój-ciąg"
  }'

Jeśli callback_url zostanie zaakceptowany, odpowiedź na utworzenie zadania będzie zawierać "webhook": "registered". callback_url jest sprawdzany od razu podczas tworzenia zadania: jeśli jest nieprawidłowy (nie zaczyna się od https lub wskazuje adres prywatny/lokalny), żądanie zwróci 400 z przyczyną ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }), a zadanie nie zostanie utworzone.

Treść webhooka ​

Gdy zadanie osiągnie stan done/failed, na Twój callback_url zostanie wysłane żądanie POST z tą samą treścią co GET /media/job/:id, uzupełnioną o 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 będzie zawierać sha256=<hmac> — HMAC-SHA256 z surowej treści żądania, z kluczem równym Twojemu sekretowi. Zweryfikuj go, aby upewnić się, że żądanie pochodzi od nas, a nie jest fałszerstwem:

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)  # porównanie w stałym czasie
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));
}

Obliczaj HMAC z dokładnie tych bajtów, które odebrano, przed parsowaniem JSON-a — w przeciwnym razie podpis się nie zgodzi.

Gotowy odbiornik w 10 liniach ​

Działający serwer, który odbiera webhook, weryfikuje podpis i odpowiada kodem 2xx. Sekret jest taki sam jak przekazany w callback_secret.

Node.js / Express:

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

const SECRET = "dowolny-twój-ciąg";             // = Twój callback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // surowa treść — wymagana do weryfikacji podpisu
  (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);             // podpis się nie zgadza — żądanie nie pochodzi od nas
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("gotowe:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // odpowiedz kodem 2xx, w przeciwnym razie żądanie zostanie ponowione
  });

app.listen(3000);

Python / FastAPI:

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

SECRET = b"dowolny-twój-ciąg"                  # = Twój callback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # surowe bajty — wymagane do weryfikacji podpisu
    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)       # podpis się nie zgadza — żądanie nie pochodzi od nas
    job = await request.json()
    if job["event"] == "media.completed":
        print("gotowe:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # odpowiedz kodem 2xx, w przeciwnym razie żądanie zostanie ponowione

Testowanie lokalne

callback_url musi być publicznym adresem https. Na potrzeby programowania na laptopie uruchom tunel, na przykład ngrok http 3000, i przekaż wygenerowany przez niego adres https jako callback_url.

Dostarczanie i niezawodność ​

  • Do 4 prób ze stopniowo wydłużaną przerwą (≈3 → 6 → 9 s), limit czasu każdej próby wynosi 15 s. Odpowiedz kodem 2xx, w przeciwnym razie żądanie zostanie ponowione.
  • Przekierowania nie są obsługiwane — odpowiadaj bezpośrednio pod adresem callback_url.
  • Dostarczanie działa na zasadzie best-effort: jeśli wszystkie 4 próby się nie powiodą (Twój serwer nie działał), webhook nie zostanie wysłany ponownie później. Dlatego:

Webhooki nie zastępują odpytywania

Webhook przyspiesza powiadomienie, ale nie daje gwarancji. Zachowaj GET /media/job/:id jako rozwiązanie zapasowe: jeśli push nie dotrze w rozsądnym czasie, samodzielnie sprawdź status. Idempotencję obsługujesz po swojej stronie: to samo zadanie może dotrzeć jako webhook i zostać wykryte podczas odpytywania — identyfikuj je po id.

Wymagania dotyczące callback_url ​

  • Tylko https://.
  • Wyłącznie publiczny adres. Adresy lokalne i prywatne (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) są odrzucane — to zabezpieczenie przed SSRF. Adres jest sprawdzany ponownie przed każdym wysłaniem (ochrona przed DNS rebindingiem).

Co wybrać ​

OdpytywanieWebhook
Publiczny adres httpsniewymaganywymagany
Opóźnienie odbiorudo następnego odpytywanianiemal natychmiast
Gwarancjamasz nad nią kontrolębest-effort + odpytywanie jako rozwiązanie zapasowe
Gdzie się sprawdzaskrypty, CI, laptopserwery, backendy produkcyjne

Rekomendacja: na backendzie używaj webhooka i odpytywania jako zabezpieczenia; w skrypcie lub na laptopie po prostu odpytuj serwer.