Skip to content

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
  1. Erstellen. POST /media/generate mit Modell und Parametern gibt eine id und einen Link zum Abfragen zurück:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Warten. Der Task durchläuft processing → done (oder failed). Bilder benötigen Sekunden, Videos und Musik einige Dutzend Sekunden bis zu ein paar Minuten.

  2. Ergebnis. Bei einem abgeschlossenen Task ist result_url ausgefüllt (Link auf nordrouter.com); die Datei wird nur für eine begrenzte Zeit gespeichert (retention_days aus GET /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:

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 — nur bei done ausgefüllt, sonst null.
  • result_url_2 — bei Musikmodellen (Suno) der zweite Track, sonst null.
  • 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 bei failed, sonst null.

Methode 1: Polling ​

Am einfachsten ist es, GET /media/job/:id regelmäßig abzufragen, bis status den Wert done oder failed annimmt:

bash
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:

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

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:

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • event ist entweder media.completed (Erfolg) oder media.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:

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)  # Vergleich in konstanter Zeit
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));
}

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:

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, иначе будет повтор

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 ​

AbfrageWebhook
Öffentliche https-Adressenicht erforderlicherforderlich
Verzögerung bis zum Empfangbis zum Abfrageintervallnahezu sofort
GarantieSie haben die KontrolleBest-Effort + Abfrage als Ausweichmöglichkeit
Geeignet fürSkripte, CI, LaptopServer, Produktiv-Backends

Empfehlung: Im Backend Webhook plus Abfrage als Absicherung; in Skripten und auf dem Laptop genügt die Abfrage.