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 processingdone (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 }
  • statusprocessing | 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.