Skip to content

Tâches et webhooks ​

La génération de médias (vidéos, images, musique) est asynchrone : vous créez une tâche (job) et récupérez le résultat lorsqu’il est prêt. Deux méthodes permettent de savoir si la tâche est terminée : l’interrogation périodique (polling) et le webhook (push). Voici comment fonctionne une tâche et comment récupérer son résultat de manière fiable.

Cycle de vie d’une tâche ​

POST /media/generate  →  status: processing  ──►  done   (result_url prêt)
        │                                     └─►  failed (error, fonds remboursés)
        └── (facultatif) callback_url → webhook push sur done/failed
  1. Création. POST /media/generate avec le modèle et les paramètres renvoie un id et un lien d’interrogation :
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Attente. La tâche passe de processing à done (ou failed). Les images prennent quelques secondes, les vidéos et la musique de quelques dizaines de secondes à quelques minutes.

  2. Résultat. Une fois la tâche terminée, result_url contient un lien vers nordrouter.com ; le fichier est conservé pour une durée limitée (retention_days dans GET /media/models) — téléchargez-le pour le conserver.

Le format de la réponse de GET /media/job/:id (et du corps du webhook — voir ci-dessous) est identique :

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 — renseigné uniquement si le statut est done, sinon null.
  • result_url_2 — deuxième piste pour les modèles musicaux (Suno), sinon null.
  • results — tous les fichiers de la tâche : [{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }] (pour la 3D : modèle et aperçu ; pour la séparation de pistes : « Voix », « Instrumental »…).
  • cost_usd — coût réel (débité à l’issue de la génération ; les générations échouées ne sont pas facturées — le montant réservé est restitué).
  • error — texte de l’erreur si le statut est failed, sinon null.

Méthode 1. Interrogation périodique (polling) ​

La méthode la plus simple consiste à appeler régulièrement GET /media/job/:id jusqu’à ce que status passe à done/failed :

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

Interrogez toutes les 2 à 3 secondes. Cette méthode fonctionne dans tous les cas et ne nécessite aucune adresse publique — elle convient aux scripts, aux notebooks et à la CI.

Méthode 2. Webhooks (push) ​

Pour éviter les interrogations répétées, transmettez callback_url à POST /media/generate et nous enverrons nous-mêmes une requête POST à votre adresse lorsque la tâche sera terminée :

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

Si callback_url est accepté, la réponse à la création contient "webhook": "registered". callback_url est vérifié immédiatement lors de la création de la tâche : s’il n’est pas valide (pas en https, adresse privée ou locale), la requête renvoie 400 avec la raison ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }) et la tâche n’est pas créée.

Corps du webhook ​

Lorsque la tâche atteint l’état done ou failed, une requête POST est envoyée à votre callback_url avec le même corps que GET /media/job/:id, ainsi qu’un champ event :

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • event vaut media.completed (succès) ou media.failed (erreur).

Vérification de la signature ​

Si vous avez défini callback_secret, l’en-tête X-NR-Signature contient sha256=<hmac> : un HMAC-SHA256 du corps brut de la requête, avec votre secret comme clé. Vérifiez-le pour vous assurer que la requête vient de nous et qu’elle n’a pas été falsifiée :

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)  # comparaison en temps constant
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));
}

Calculez le HMAC à partir des octets reçus tels quels, avant l’analyse JSON, sinon la signature ne correspondra pas.

Récepteur prêt à l’emploi en 10 lignes ​

Un serveur opérationnel qui reçoit le webhook, vérifie la signature et répond 2xx. Le secret est celui que vous avez transmis dans 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, иначе будет повтор

Vérifier en local

callback_url doit être une adresse https publique. Pour le développement sur votre ordinateur portable, établissez un tunnel, par exemple avec ngrok http 3000, puis transmettez l’adresse https fournie comme callback_url.

Livraison et fiabilité ​

  • Jusqu’à 4 tentatives, avec un délai croissant (≈3 → 6 → 9 s) ; chaque tentative expire au bout de 15 s. Répondez 2xx, sinon une nouvelle tentative sera effectuée.
  • Les redirections ne sont pas suivies : répondez directement à l’adresse callback_url.
  • La livraison est en best-effort : si les 4 tentatives échouent (votre serveur était indisponible), le webhook n’est pas renvoyé ultérieurement. Par conséquent :

Les webhooks ne remplacent pas l’interrogation

Un webhook accélère la réception, mais ne la garantit pas. Gardez GET /media/job/:id comme solution de secours : si le push n’arrive pas dans un délai raisonnable, interrogez vous-même le statut. L’idempotence est de votre responsabilité : une même tâche peut être reçue par webhook et repérée par interrogation ; basez-vous sur id.

Exigences relatives à callback_url ​

  • Utilisez uniquement https://.
  • L’adresse doit être publique. Les adresses locales et privées (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) sont rejetées pour se protéger contre les attaques SSRF. L’adresse est vérifiée à nouveau avant chaque envoi, afin de se protéger contre le DNS rebinding.

Que choisir ​

InterrogationWebhook
Adresse https publiquenon requiserequise
Délai de réceptionjusqu’à l’intervalle d’interrogationpresque instantané
Garantievous gardez le contrôlebest-effort + interrogation de secours
Utilisation recommandéescripts, CI, ordinateur portableserveurs, backends de production

Recommandation : sur un backend, utilisez un webhook et gardez l’interrogation comme solution de secours ; dans un script ou sur un ordinateur portable, contentez-vous de l’interrogation.