Skip to content

Tarefas e webhooks ​

A geração de mídia (vídeo, imagens, música) é assíncrona: você cria uma tarefa (job) e obtém o resultado quando estiver pronto. Há duas formas de saber que a tarefa foi concluída: consulta periódica (polling) e webhook (push). Veja abaixo como uma tarefa funciona e como obter o resultado de forma confiável.

Ciclo de vida da tarefa ​

POST /media/generate  →  status: processing  ──►  done   (result_url pronto)
        │                                     └─►  failed (error, saldo devolvido)
        └── (opcional) callback_url → webhook push em done/failed
  1. Criação. POST /media/generate com o modelo e os parâmetros — retorna id e um link para consulta:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Espera. A tarefa passa de processing → done (ou failed). Imagens levam segundos; vídeos e músicas, de dezenas de segundos a alguns minutos.

  2. Resultado. Uma tarefa concluída tem o campo result_url preenchido (link em nordrouter.com); o arquivo fica armazenado por tempo limitado (retention_days de GET /media/models) — faça o download para guardá-lo.

O formato da resposta de GET /media/job/:id (e do corpo do webhook — veja abaixo) é o mesmo:

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 — preenchido apenas em done; caso contrário, null.
  • result_url_2 — segunda faixa nos modelos de música (Suno); caso contrário, null.
  • results — todos os arquivos da tarefa: [{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }] (para 3D, o modelo e a prévia; para separação de faixas, as pistas “Vocal”, “Instrumental”…).
  • cost_usd — custo efetivo (cobrado ao final; gerações com falha não são cobradas — o valor reservado é devolvido).
  • error — texto do erro em failed; caso contrário, null.

Método 1. Consulta periódica (polling) ​

A forma mais simples é consultar GET /media/job/:id periodicamente até que status seja done/failed:

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

Consulte a cada 2–3 segundos. A consulta periódica funciona sempre e não exige um endereço público — é adequada para scripts, notebooks e CI.

Método 2. Webhooks (push) ​

Para evitar consultas periódicas, passe callback_url em POST /media/generate, e nós enviaremos um POST para o seu endereço quando a tarefa terminar:

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": "qualquer-string-sua"
  }'

Se callback_url for aceito, a resposta de criação conterá "webhook": "registered". O callback_url é validado imediatamente na criação da tarefa: se não atender aos requisitos (não usar https ou for um endereço privado/local), a requisição retornará 400 com o motivo ({ "error": "callback_url rejeitado: …", "code": "invalid_callback" }), e a tarefa não será criada.

Corpo do webhook ​

Quando a tarefa chega a done/failed, seu callback_url recebe um POST com o mesmo corpo de GET /media/job/:id, mais o 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 (sucesso) ou media.failed (erro).

Verificação da assinatura ​

Se você definiu callback_secret, o cabeçalho X-NR-Signature contém sha256=<hmac> — o HMAC-SHA256 do corpo bruto da requisição, usando seu segredo como chave. Verifique-o para confirmar que a requisição veio de nós e não foi falsificada:

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)  # comparação em tempo 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));
}

Calcule o HMAC dos bytes exatamente como foram recebidos, antes de fazer o parsing do JSON — caso contrário, a assinatura não corresponderá.

Receptor pronto em 10 linhas ​

Um servidor funcional que recebe o webhook, verifica a assinatura e responde com 2xx. O segredo é o mesmo que você passou em callback_secret.

Node.js / Express:

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

const SECRET = "qualquer-string-sua";           // = seu callback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // corpo bruto — obrigatório para a assinatura
  (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);             // assinatura inválida — a requisição não veio de nós
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("concluído:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // responda com 2xx, caso contrário haverá nova tentativa
  });

app.listen(3000);

Python / FastAPI:

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

SECRET = b"qualquer-string-sua"                  # = seu callback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # bytes brutos — obrigatórios para a assinatura
    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)       # assinatura inválida — a requisição não veio de nós
    job = await request.json()
    if job["event"] == "media.completed":
        print("concluído:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # responda com 2xx, caso contrário haverá nova tentativa

Testar localmente

O callback_url deve ser um endereço https público. Para desenvolver no notebook, crie um túnel — por exemplo, ngrok http 3000 — e passe o endereço https gerado como callback_url.

Entrega e confiabilidade ​

  • Até 4 tentativas com intervalos crescentes (≈3 → 6 → 9 s), com timeout de 15 s por tentativa. Responda com 2xx, caso contrário haverá nova tentativa.
  • Redirecionamentos não são seguidos — responda diretamente no callback_url.
  • A entrega é best-effort: se todas as 4 tentativas falharem (seu servidor estava fora do ar), o webhook não será reenviado depois. Portanto:

Webhooks não substituem as consultas periódicas

O webhook agiliza o processo, mas não é uma garantia. Mantenha GET /media/job/:id como alternativa: se o push não chegar em um tempo razoável, consulte o status. A idempotência é responsabilidade sua: a mesma tarefa pode chegar por webhook e ser detectada em uma consulta — use o id como referência.

Requisitos para callback_url ​

  • Apenas https://.
  • Apenas endereços públicos. Endereços locais e privados (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) são rejeitados — isso protege contra SSRF. O endereço também é verificado novamente antes de cada envio (proteção contra DNS rebinding).

O que escolher ​

Consultas periódicasWebhook
Endereço https públiconão é necessárionecessário
Latência de recebimentoaté o intervalo entre consultasquase instantâneo
Garantiavocê controlabest-effort + consultas como alternativa
Onde é convenientescripts, CI, notebookservidores, backends de produção

Recomendação: no backend — webhook + consultas periódicas como garantia adicional; em scripts/notebook — apenas consultas periódicas.