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- Criação.
POST /media/generatecom o modelo e os parâmetros — retornaide um link para consulta:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Espera. A tarefa passa de
processing→done(oufailed). Imagens levam segundos; vídeos e músicas, de dezenas de segundos a alguns minutos.Resultado. Uma tarefa concluída tem o campo
result_urlpreenchido (link emnordrouter.com); o arquivo fica armazenado por tempo limitado (retention_daysdeGET /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:
{ "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 emdone; 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 emfailed; 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:
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:
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:
{ "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) oumedia.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:
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 constanteimport 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:
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:
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 tentativaTestar 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ódicas | Webhook | |
|---|---|---|
Endereço https público | não é necessário | necessário |
| Latência de recebimento | até o intervalo entre consultas | quase instantâneo |
| Garantia | você controla | best-effort + consultas como alternativa |
| Onde é conveniente | scripts, CI, notebook | servidores, backends de produção |
Recomendação: no backend — webhook + consultas periódicas como garantia adicional; em scripts/notebook — apenas consultas periódicas.