Skip to content

Attività e webhook ​

La generazione di contenuti multimediali (video, immagini, musica) è asincrona: si crea un'attività (job) e si recupera il risultato quando è pronto. Esistono due modi per sapere quando l'attività è terminata: polling e webhook (push). Di seguito sono descritti la struttura di un'attività e come recuperarne il risultato in modo affidabile.

Ciclo di vita di un'attività ​

POST /media/generate  →  status: processing  ──►  done   (result_url pronto)
        │                                     └─►  failed (error, fondi rimborsati)
        └── (opzionale) callback_url → webhook push su done/failed
  1. Creazione. POST /media/generate con il modello e i parametri restituisce id e un link per il polling:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Attesa. L'attività passa da processing a done (o failed). Le immagini richiedono pochi secondi, i video e la musica da qualche decina di secondi a un paio di minuti.

  2. Risultato. Un'attività completata ha il campo result_url valorizzato (un link su nordrouter.com); il file viene conservato per un periodo limitato (retention_days da GET /media/models): scaricarlo per conservarlo.

Il formato della risposta di GET /media/job/:id (e del corpo del webhook, vedi sotto) è identico:

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 — valorizzato solo con done, altrimenti null.
  • result_url_2 — seconda traccia per i modelli musicali (Suno), altrimenti null.
  • results — tutti i file dell'attività: [{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }] (per il 3D, modello e anteprima; per la separazione di una traccia, le tracce «Voce», «Strumentale»…).
  • cost_usd — costo effettivo (addebitato a consuntivo; le generazioni non riuscite non vengono addebitate e l'importo riservato viene restituito).
  • error — testo dell'errore con failed, altrimenti null.

Metodo 1. Polling ​

Il metodo più semplice consiste nel richiedere periodicamente GET /media/job/:id finché status non diventa done/failed:

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

Eseguire il polling ogni 2–3 secondi. Il polling funziona sempre e non richiede un indirizzo pubblico: è adatto a script, notebook e CI.

Metodo 2. Webhook (push) ​

Per evitare di interrogare lo stato, passa callback_url a POST /media/generate: invieremo noi una richiesta POST al tuo indirizzo quando l’attività sarà terminata:

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": "una-tua-stringa"
  }'

Se callback_url viene accettato, la risposta alla creazione contiene "webhook": "registered". callback_url viene verificato subito al momento della creazione dell’attività: se non è valido (non usa https oppure punta a un indirizzo privato/locale), la richiesta restituisce 400 con il motivo ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }) e l’attività non viene creata.

Corpo del webhook ​

Quando l’attività raggiunge lo stato done/failed, il tuo callback_url riceve una richiesta POST con lo stesso corpo di GET /media/job/:id, più il campo event:

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • event — media.completed (successo) oppure media.failed (errore).

Verifica della firma ​

Se hai specificato callback_secret, l’header X-NR-Signature contiene sha256=<hmac> — un HMAC-SHA256 del corpo grezzo della richiesta, con la tua chiave segreta. Verificalo per accertarti che la richiesta provenga da noi e non sia contraffatta:

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)  # confronto a tempo costante
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));
}

Calcola l’HMAC sui byte esattamente come ricevuti, prima di analizzare il JSON; altrimenti la firma non corrisponderà.

Ricevitore pronto in 10 righe ​

Un server funzionante che riceve il webhook, verifica la firma e risponde con 2xx. Usa lo stesso segreto passato in callback_secret.

Node.js / Express:

javascript
import express from "express";
import crypto from "node:crypto";

const SECRET = "una-tua-stringa";           // = il tuo callback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // corpo grezzo: obbligatorio per la firma
  (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);             // firma non corrispondente: la richiesta non proviene da noi
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("completato:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // rispondi con 2xx, altrimenti il webhook verrà ritentato
  });

app.listen(3000);

Python / FastAPI:

python
import hmac, hashlib
from fastapi import FastAPI, Request, Response

SECRET = b"una-tua-stringa"                  # = il tuo callback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # byte grezzi: obbligatori per la firma
    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)       # firma non corrispondente: la richiesta non proviene da noi
    job = await request.json()
    if job["event"] == "media.completed":
        print("completato:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # rispondi con 2xx, altrimenti il webhook verrà ritentato

Verifica in locale

callback_url deve essere un indirizzo https pubblico. Per lo sviluppo sul portatile, avvia un tunnel, ad esempio ngrok http 3000, e passa come callback_url l’indirizzo https che ti viene assegnato.

Consegna e affidabilità ​

  • Fino a 4 tentativi con pause crescenti (≈3 → 6 → 9 s); timeout di ciascun tentativo: 15 s. Rispondi con 2xx, altrimenti la richiesta verrà ritentata.
  • I reindirizzamenti non vengono seguiti: rispondi direttamente all’indirizzo callback_url.
  • La consegna è best-effort: se tutti e 4 i tentativi falliscono (ad esempio, il tuo server è fuori servizio), il webhook non verrà inviato di nuovo in seguito. Pertanto:

I webhook non sostituiscono il polling

Il webhook è un modo per accelerare, non una garanzia. Mantieni GET /media/job/:id come alternativa: se il push non arriva entro un tempo ragionevole, controlla lo stato manualmente. L’idempotenza è a carico tuo: la stessa attività può arrivare come webhook e essere rilevata dal polling; fai riferimento a id.

Requisiti di callback_url ​

  • Solo https://.
  • Solo indirizzi pubblici. Gli indirizzi locali e privati (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) vengono rifiutati per proteggere dagli attacchi SSRF. L’indirizzo viene verificato di nuovo prima di ogni invio, per proteggere dal DNS rebinding.

Quale scegliere ​

PollingWebhook
Indirizzo https pubbliconon necessarionecessario
Ritardo nella ricezionefino all’intervallo di pollingquasi immediato
Garanziasotto il tuo controllobest-effort + polling come alternativa
Ideale perscript, CI, portatileserver, backend di produzione

Consiglio: per un backend, usa webhook + polling come misura di sicurezza; per uno script o un portatile, basta il polling.