Skip to content

Tarefas e webhooks

A geração de mídia (vídeo, imagens, música) é assíncrona: você cria uma tarefa (trabalho) e pega o resultado quando estiver pronto. Há duas maneiras de descobrir que uma tarefa foi concluída: poll (sondagem) e webhook (push). Abaixo está como a tarefa funciona e como obter resultados confiáveis.

Ciclo de vida da tarefa

POST /media/generate  →  status: processing  ──►  done   (result_url готов)
        │                                     └─►  failed (error, средства возвращены)
        └── (опционально) callback_url → пуш-вебхук на done/failed
  1. Criação. POST /media/generate com modelo e parâmetros - retorna id e um link para a pesquisa:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Aguardando. A tarefa passa processingdone (ou failed). Imagens - segundos, vídeo e música - de dezenas de segundos a alguns minutos.

  2. Resultado. A tarefa concluída tem result_url preenchido (link para nordrouter.com); o arquivo é armazenado por tempo limitado (retention_days de GET /media/models) - baixe para salvar.

O formato da resposta GET /media/job/:id (e o 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 }
  • statusprocessing | done | failed.
  • result_url - preenchido apenas com done, caso contrário null.
  • result_url_2 - a segunda faixa para modelos musicais (Suno), caso contrário null.
  • cost_usd - custo real (baixado após o fato; geração malsucedida não cobrada - a reserva é devolvida).
  • error – texto de erro para failed, caso contrário, null.

Método 1. Votação

A maneira mais fácil é solicitar periodicamente GET /media/job/:id até que status se torne done/failed:

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

Faça pesquisas uma vez a cada 2-3 segundos. A pesquisa sempre funciona e não requer endereço público - adequado para scripts, laptops, CI.

Método 2. Webhooks (push)

Para evitar polling, passe callback_url para POST /media/generate, e enviaremos um POST para o seu endereço quando a tarefa for concluída:

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": "любая-ваша-строка"
  }'

Se callback_url for aceito, a resposta de criação conterá "webhook": "registered". callback_url é verificado imediatamente ao criar uma tarefa: se não for adequado (não https, endereço privado/local) - a solicitação retornará 400 com um motivo, a tarefa não foi criada.

Corpo do webhook

Quando a tarefa chega a done/failed, seu callback_url recebe 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 de assinatura

Se você definir callback_secret, o cabeçalho X-NR-Signature conterá sha256=<hmac> - HMAC-SHA256 do corpo da solicitação bruta, com chave = seu segredo. Verifique para ter certeza de que a solicitação é nossa e não falsa:

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)  # сравнение за константное время
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));
}

Leia HMAC de bytes exatamente como recebidos para análise JSON - caso contrário, a assinatura não corresponderá.

Receptor pronto em 10 linhas

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

Node.js / Express:

javascript
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:

python
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, иначе будет повтор

Verifique localmente

callback_url deve ser público https. Para desenvolver em um laptop, crie um túnel - por exemplo ngrok http 3000 - e passe o endereço https emitido por ele como callback_url.

Entrega e confiabilidade

  • Até 4 tentativas com pausa crescente (≈3 → 6 → 9 s), cada tempo limite é de 15 s. Responda 2xx, caso contrário será repetido.
  • Redirecionamentos não funcionam - responda diretamente para callback_url.
  • Entrega - melhor esforço: se todas as 4 tentativas falharem (seu servidor estiver inativo), o webhook não será reenviado posteriormente. Portanto:

Webhooks não substituem pesquisas

Um webhook é uma aceleração, não uma garantia. Mantenha GET /media/job/:id como rota de backup: se o push não chegar dentro de um tempo razoável, pesquise você mesmo o status. A idempotência está do seu lado: a mesma tarefa pode vir como um webhook e ser notada por uma pesquisa - concentre-se em id.

Requisitos para callback_url

  • Apenas https://.
  • Somente endereço público. 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, loopback IPv6/ULA/link-local) rejeitar é proteção SSRF. O endereço é verificado duas vezes antes de cada envio (proteção contra religação de DNS).

O que escolher

PesquisaWebhook
Endereço público httpsnão necessárionecessário
Atraso no recebimento deaté o intervalo de pesquisaser quase instantâneo
Garantiavocê controlamelhor esforço + pesquisa como reserva
Onde for convenientescripts, CI, laptopservidores, backends de produção

Recomendação: no backend - webhook + pesquisa como seguro; em um script/laptop - apenas uma pesquisa.