Tasks und Webhooks
Die Generierung von Medien (Videos, Bilder, Musik) erfolgt asynchron: Sie erstellen einen Task (Job) und rufen das Ergebnis ab, sobald es bereit ist. Es gibt zwei Möglichkeiten, zu erfahren, dass ein Task abgeschlossen ist: Polling und Webhook (Push). Im Folgenden erfahren Sie, wie ein Task funktioniert und wie Sie das Ergebnis zuverlässig abrufen.
Lebenszyklus eines Tasks
POST /media/generate → status: processing ──► done (result_url bereit)
│ └─► failed (error, Guthaben zurückerstattet)
└── (optional) callback_url → Push-Webhook bei done/failed- Erstellen.
POST /media/generatemit Modell und Parametern gibt eineidund einen Link zum Abfragen zurück:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Warten. Der Task durchläuft
processing→done(oderfailed). Bilder benötigen Sekunden, Videos und Musik einige Dutzend Sekunden bis zu ein paar Minuten.Ergebnis. Bei einem abgeschlossenen Task ist
result_urlausgefüllt (Link aufnordrouter.com); die Datei wird nur für eine begrenzte Zeit gespeichert (retention_daysausGET /media/models). Laden Sie sie herunter, um sie dauerhaft zu speichern.
Das Antwortformat von GET /media/job/:id (und des Webhook-Bodys, siehe unten) ist identisch:
{ "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }status—processing|done|failed.result_url— nur beidoneausgefüllt, sonstnull.result_url_2— bei Musikmodellen (Suno) der zweite Track, sonstnull.results— alle Dateien des Tasks:[{ "index": 1, "name": "Ergebnis 1", "ext": "png", "url": "…" }](bei 3D das Modell und die Vorschau, bei der Tracktrennung die Spuren „Gesang“, „Instrumental“ …).cost_usd— tatsächliche Kosten (werden nach Aufwand berechnet; fehlgeschlagene Generierungen werden nicht berechnet — die Reservierung wird zurückerstattet).error— Fehlermeldung beifailed, sonstnull.
Methode 1: Polling
Am einfachsten ist es, GET /media/job/:id regelmäßig abzufragen, bis status den Wert done oder failed annimmt:
curl https://nordrouter.com/media/job/beefb531-…Fragen Sie alle 2–3 Sekunden ab. Polling funktioniert immer und erfordert keine öffentliche Adresse — es eignet sich für Skripte, Notebooks und CI.
Methode 2: Webhooks (Push)
Um nicht ständig abzufragen, übergeben Sie callback_url an POST /media/generate. Sobald die Aufgabe abgeschlossen ist, senden wir selbst eine POST-Anfrage an Ihre Adresse:
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": "любая-ваша-строка"
}'Wenn callback_url akzeptiert wurde, enthält die Antwort auf die Erstellung "webhook": "registered". callback_url wird sofort beim Erstellen der Aufgabe geprüft: Ist die URL ungültig (nicht https, private/lokale Adresse), wird die Anfrage mit 400 und dem Grund zurückgewiesen ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }); die Aufgabe wird nicht erstellt.
Webhook-Body
Sobald die Aufgabe den Status done/failed erreicht, wird eine POST-Anfrage mit demselben Body wie bei GET /media/job/:id plus dem Feld event an Ihre callback_url gesendet:
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }eventist entwedermedia.completed(Erfolg) odermedia.failed(Fehler).
Signatur prüfen
Wenn Sie callback_secret festgelegt haben, enthält der Header X-NR-Signature den Wert sha256=<hmac> — ein HMAC-SHA256 über den rohen Request-Body, wobei Ihr Secret als Schlüssel dient. Prüfen Sie die Signatur, um sicherzustellen, dass die Anfrage von uns stammt und nicht gefälscht ist:
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) # Vergleich in konstanter Zeitimport 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));
}Berechnen Sie das HMAC über die Bytes genau so, wie sie empfangen wurden, noch vor dem Parsen des JSON. Andernfalls stimmt die Signatur nicht.
Empfangsserver in 10 Zeilen
Ein funktionsfähiger Server, der den Webhook empfängt, die Signatur prüft und mit 2xx antwortet. Das Secret ist dasselbe, das Sie in callback_secret übergeben haben.
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, иначе будет повторLokal testen
callback_url muss öffentlich und https sein. Richten Sie für die Entwicklung auf Ihrem Laptop einen Tunnel ein, zum Beispiel ngrok http 3000, und übergeben Sie die bereitgestellte https-Adresse als callback_url.
Zustellung und Zuverlässigkeit
- Bis zu 4 Versuche mit zunehmenden Abständen (≈3 → 6 → 9 s), Timeout pro Versuch: 15 s. Antworten Sie mit
2xx, andernfalls wird die Anfrage wiederholt. - Weiterleitungen werden nicht verfolgt — antworten Sie direkt unter
callback_url. - Die Zustellung erfolgt nach Best-Effort-Prinzip: Schlagen alle 4 Versuche fehl (weil Ihr Server nicht erreichbar war), wird der Webhook später nicht erneut gesendet. Deshalb:
Webhooks ersetzen die Abfrage nicht
Ein Webhook beschleunigt die Zustellung, bietet aber keine Garantie. Verwenden Sie GET /media/job/:id als Ausweichmöglichkeit: Wenn der Push nicht innerhalb einer angemessenen Zeit eintrifft, fragen Sie den Status selbst ab. Für die Idempotenz sind Sie selbst verantwortlich: Dieselbe Aufgabe kann sowohl als Webhook eintreffen als auch bei einer Statusabfrage erkannt werden. Orientieren Sie sich an id.
Anforderungen an callback_url
- Nur
https://. - Nur eine öffentliche Adresse. Lokale und private Adressen (
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) werden abgelehnt — zum Schutz vor SSRF. Die Adresse wird auch vor jedem Versand erneut geprüft, um DNS-Rebinding zu verhindern.
Was Sie wählen sollten
| Abfrage | Webhook | |
|---|---|---|
Öffentliche https-Adresse | nicht erforderlich | erforderlich |
| Verzögerung bis zum Empfang | bis zum Abfrageintervall | nahezu sofort |
| Garantie | Sie haben die Kontrolle | Best-Effort + Abfrage als Ausweichmöglichkeit |
| Geeignet für | Skripte, CI, Laptop | Server, Produktiv-Backends |
Empfehlung: Im Backend Webhook plus Abfrage als Absicherung; in Skripten und auf dem Laptop genügt die Abfrage.