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- Création.
POST /media/generateavec le modèle et les paramètres renvoie unidet un lien d’interrogation :
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Attente. La tâche passe de
processingàdone(oufailed). Les images prennent quelques secondes, les vidéos et la musique de quelques dizaines de secondes à quelques minutes.Résultat. Une fois la tâche terminée,
result_urlcontient un lien versnordrouter.com; le fichier est conservé pour une durée limitée (retention_daysdansGET /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 :
{ "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 estdone, sinonnull.result_url_2— deuxième piste pour les modèles musicaux (Suno), sinonnull.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 estfailed, sinonnull.
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 :
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 :
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 :
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }eventvautmedia.completed(succès) oumedia.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 :
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 constantimport 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 :
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, иначе будет повтор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
| Interrogation | Webhook | |
|---|---|---|
Adresse https publique | non requise | requise |
| Délai de réception | jusqu’à l’intervalle d’interrogation | presque instantané |
| Garantie | vous gardez le contrôle | best-effort + interrogation de secours |
| Utilisation recommandée | scripts, CI, ordinateur portable | serveurs, 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.