Skip to content

Tareas y webhooks ​

La generación de contenido multimedia (vídeo, imágenes, música) es asíncrona: se crea una tarea (job) y se obtiene el resultado cuando está listo. Hay dos formas de saber que una tarea ha finalizado: sondeo (polling) y webhook (push). A continuación se explica cómo funciona una tarea y cómo obtener el resultado de forma fiable.

Ciclo de vida de una tarea ​

POST /media/generate  →  status: processing  ──►  done   (result_url listo)
        │                                     └─►  failed (error, se reembolsan los fondos)
        └── (opcional) callback_url → webhook push al completar o fallar
  1. Creación. POST /media/generate con el modelo y los parámetros devuelve id y un enlace para sondear:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Espera. La tarea pasa de processing a done (o failed). Las imágenes tardan segundos; el vídeo y la música, desde decenas de segundos hasta un par de minutos.

  2. Resultado. Cuando la tarea termina, result_url contiene un enlace a nordrouter.com. El archivo se conserva durante un tiempo limitado (retention_days de GET /media/models); descárguelo para guardarlo.

El formato de respuesta de GET /media/job/:id (y del cuerpo del webhook; véase más abajo) es el mismo:

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: solo está presente cuando el estado es done; en caso contrario, es null.
  • result_url_2: segundo tema en los modelos de música (Suno); en caso contrario, null.
  • results: todos los archivos de la tarea: [{ "index": 1, "name": "Resultado 1", "ext": "png", "url": "…" }] (en 3D incluye el modelo y la vista previa; al separar pistas, las pistas «Voz», «Instrumental»…).
  • cost_usd: coste real (se cobra según el uso; las generaciones fallidas no se cobran y se reembolsa la reserva).
  • error: texto del error cuando el estado es failed; en caso contrario, null.

Método 1. Sondeo (polling) ​

La forma más sencilla es consultar periódicamente GET /media/job/:id hasta que status sea done o failed:

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

Consulte cada 2–3 segundos. El sondeo siempre funciona y no requiere una dirección pública, por lo que es adecuado para scripts, notebooks y CI.

Método 2. Webhooks (push) ​

Para evitar el sondeo, envía callback_url en POST /media/generate y enviaremos un POST a tu dirección cuando finalice la tarea:

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": "cualquier-cadena-tuya"
  }'

Si se acepta callback_url, la respuesta de creación contiene "webhook": "registered". callback_url se valida de inmediato al crear la tarea: si no cumple los requisitos (no usa https o es una dirección privada/local), la solicitud devolverá 400 con el motivo ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }) y no se creará la tarea.

Cuerpo del webhook ​

Cuando la tarea alcanza el estado done/failed, tu callback_url recibe un POST con el mismo cuerpo que GET /media/job/:id, más el 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 (éxito) o media.failed (error).

Verificación de la firma ​

Si has definido callback_secret, la cabecera X-NR-Signature contiene sha256=<hmac>: el HMAC-SHA256 del cuerpo sin procesar de la solicitud, usando tu secreto como clave. Verifícalo para comprobar que la solicitud proviene de nosotros y no es una falsificación:

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)  # comparación en tiempo constante
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));
}

Calcula el HMAC sobre los bytes exactamente como llegaron, antes de analizar el JSON; de lo contrario, la firma no coincidirá.

Receptor listo en 10 líneas ​

Un servidor funcional que recibe el webhook, verifica la firma y responde con 2xx. El secreto es el mismo que enviaste en callback_secret.

Node.js / Express:

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

const SECRET = "cualquier-cadena-tuya";           // = tu callback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // cuerpo sin procesar: obligatorio para 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);             // la firma no coincide: la solicitud no es nuestra
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("listo:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // responde con 2xx; de lo contrario, se reintentará
  });

app.listen(3000);

Python / FastAPI:

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

SECRET = b"cualquier-cadena-tuya"                  # = tu callback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # bytes sin procesar: obligatorios para 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)       # la firma no coincide: la solicitud no es nuestra
    job = await request.json()
    if job["event"] == "media.completed":
        print("listo:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # responde con 2xx; de lo contrario, se reintentará

Prueba local

callback_url debe ser una dirección https pública. Para desarrollar en un portátil, crea un túnel, por ejemplo con ngrok http 3000, y usa la dirección https que te proporcione como callback_url.

Entrega y fiabilidad ​

  • Hasta 4 intentos con pausas crecientes (≈3 → 6 → 9 s) y un tiempo de espera de 15 s por intento. Responde con 2xx; de lo contrario, se reintentará.
  • No se siguen las redirecciones: responde directamente en callback_url.
  • La entrega es best-effort: si fallan los 4 intentos (tu servidor estaba caído), el webhook no se reenviará más tarde. Por lo tanto:

Los webhooks no sustituyen al sondeo

El webhook acelera la notificación, pero no ofrece garantías. Mantén GET /media/job/:id como alternativa: si la notificación push no llega en un plazo razonable, consulta el estado por tu cuenta. La idempotencia corre de tu parte: una misma tarea puede llegar por webhook y detectarse mediante sondeo; usa id como referencia.

Requisitos de callback_url ​

  • Solo https://.
  • Solo direcciones públicas. Las direcciones locales y privadas (localhost, *.local, 10.x, 172.16–31.x, 192.168.x, 127.x, 169.254.x, 100.64–127.x, loopback/ULA/link-local de IPv6) se rechazan como protección contra SSRF. La dirección también se vuelve a comprobar antes de cada envío (protección contra DNS rebinding).

Qué elegir ​

SondeoWebhook
Dirección https públicano es necesariaes necesaria
Latencia de recepciónhasta el intervalo de sondeocasi instantánea
Garantíatú tienes el controlbest-effort + sondeo como respaldo
Dónde resulta útilscripts, CI, portátilservidores, backends de producción

Recomendación: en el backend, webhook + sondeo como respaldo; en un script/portátil, solo sondeo.