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- Utworzenie.
POST /media/generatez modelem i parametrami - zwracaidi link do ankiety:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Oczekiwanie. Zadanie przechodzi
processing→done(lubfailed). Obrazy - sekundy, wideo i muzyka - od kilkudziesięciu sekund do kilku minut.Wynik. Ukończone zadanie ma wypełniony
result_url(link donordrouter.com); plik jest przechowywany przez ograniczony czas (retention_dayszGET /media/models) - pobierz, aby zapisać.
Format odpowiedzi GET /media/job/:id (i treści webhooka - patrz poniżej) jest taki sam:
{ "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ę tylkodone, w przeciwnym razienull.result_url_2- drugi utwór dla modeli muzycznych (Suno), inaczejnull.cost_usd- koszt rzeczywisty (odpisany po fakcie; nieudana generacja nie naliczona - rezerwa zostaje zwrócona).error- tekst błędu dlafailed, w przeciwnym razienull.
Metoda 1. Odpytywanie
Najłatwiej jest okresowo wysyłać żądanie GET /media/job/:id, aż status zmieni się na done/failed:
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:
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:
{ "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) lubmedia.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:
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) # сравнение за константное время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:
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:
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ć
| Ankieta | Webhook | |
|---|---|---|
Publiczny https adres | niepotrzebny | potrzebny |
| Opóźnienie odbioru | do momentu, aż interwał odpytywania | będzie prawie natychmiastowy |
| Gwarancja | kontrolujesz | best-effort + ankieta jako rezerwa |
| Gdzie wygodnie | skrypty, CI, laptop | serwery, backendy produkcyjne |
Rekomendacja: na backendzie - webhook + ankieta jako zabezpieczenie; w skrypcie/laptopie - wystarczy ankieta.