PopNowDocs da API
Construindo com a API
Conteúdo principal

Webhooks

Receba tarefas concluídas na sua própria URL, verifique a assinatura e lide com novas tentativas e duplicatas.

Com um webhook, a PopNow avisa quando uma tarefa termina e você não precisa ficar perguntando. Existem dois tipos:

Callback de tarefaEndpoint da conta
ConfiguraçãocallBackUrl em cada createTaskUma vez, no console da API
Enviado paraAquela tarefaToda tarefa da conta que bate com os eventos escolhidos
AssinaturaX-Webhook-Signature (abaixo)Cabeçalhos do Standard Webhooks (veja abaixo)

A maioria das integrações só precisa do callback de tarefa.

1. Pegue seu segredo de assinatura

Toda entrega é assinada com o segredo de assinatura de webhooks da sua conta. Abra a página Webhooks do console da API, gere (ou rotacione) o segredo e guarde no servidor como POPNOW_WEBHOOK_SECRET. Ele aparece uma única vez. A rotação vale na hora, inclusive para entregas ainda em nova tentativa, então atualize seus receptores logo depois.

2. Passe o callBackUrl

{
  "model": "alibaba/wan-3.0/image-to-video",
  "input": { "image_url": "https://media.popnow.ai/movie/lighthouse.webp" },
  "callBackUrl": "https://your-app.example.com/hooks/popnow"
}

A URL precisa ser HTTPS pública na porta 443 (ou sem porta), sem usuário nem senha, e resolver apenas para endereços públicos — isso é verificado ao criar a tarefa e de novo na conexão. http://localhost não tem como funcionar; durante o desenvolvimento, use um túnel como Cloudflare Tunnel ou ngrok, ou faça polling. Uma URL inválida retorna 400 com msg: "Invalid callback URL".

3. O que chega

Quando a tarefa chega a um estado final, a PopNow envia um POST:

POST /hooks/popnow HTTP/1.1
Content-Type: application/json
User-Agent: PopNow-Webhook/1
X-Webhook-Timestamp: 1787045531
X-Webhook-Signature: 9tXk3…=
X-Webhook-Payload-Version: 2

O corpo é exatamente o que o recordInfo devolveria para a tarefa:

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01JH8Z4Q5W2N7R3K9M6P1C0VXD",
    "model": "alibaba/wan-3.0/image-to-video",
    "state": "succeeded",
    "output": { "assets": [{ "url": "https://…", "mime": "video/mp4", "durationSeconds": 5 }] },
    "credits": 34,
    "cost": "0.34",
    "settled": true
  },
  "request_id": "whd_01JH8Z…"
}
  • code é 200 mesmo para uma tarefa com falha: ele descreve a entrega, não a tarefa. Leia data.state.
  • request_id é o ID da entrega. Ele não muda entre tentativas, então use-o para ignorar duplicatas.
  • Os links assinados em output expiram em uns 20 minutos. Guarde o taskId, não o link.

4. Verifique a assinatura

signature = base64( HMAC-SHA256( secret, "<taskId>.<timestamp>.<sha256_hex(raw_body)>" ) )

taskId é o data.taskId do corpo, timestamp é o cabeçalho X-Webhook-Timestamp, e a última parte é o SHA-256 do corpo bruto da requisição, em hexadecimal minúsculo. Faça o hash dos bytes que você recebeu, não de um objeto reserializado: a ordem das chaves e os espaços mudariam o resultado.

Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.POPNOW_WEBHOOK_SECRET;

// express.raw keeps the exact bytes; express.json would throw them away.
app.post('/hooks/popnow', express.raw({ type: 'application/json', limit: '10mb' }), (req, res) => {
  const ts = Number(req.headers['x-webhook-timestamp']);
  const sig = String(req.headers['x-webhook-signature'] ?? '');
  // Refuse anything older than five minutes, however valid its signature.
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end();

  let event;
  try { event = JSON.parse(req.body.toString('utf8')); } catch { return res.status(400).end(); }
  const digest = crypto.createHash('sha256').update(req.body).digest('hex');
  const expected = crypto.createHmac('sha256', SECRET)
    .update(`${event.data?.taskId}.${ts}.${digest}`).digest('base64');
  const ok = expected.length === sig.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  if (!ok) return res.status(401).end();

  res.status(200).end();          // acknowledge first…
  enqueue(event.request_id, event.data); // …then do the work
});
Python (Flask)
import hashlib, hmac, base64, json, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["POPNOW_WEBHOOK_SECRET"].encode()

@app.post("/hooks/popnow")
def popnow_hook():
    raw = request.get_data()  # the exact bytes
    ts = request.headers.get("X-Webhook-Timestamp", "")
    sig = request.headers.get("X-Webhook-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(401)
    event = json.loads(raw)
    digest = hashlib.sha256(raw).hexdigest()
    message = f'{event["data"]["taskId"]}.{ts}.{digest}'.encode()
    expected = base64.b64encode(hmac.new(SECRET, message, hashlib.sha256).digest()).decode()
    if not hmac.compare_digest(expected, sig):
        abort(401)
    enqueue(event["request_id"], event["data"])  # do the slow work elsewhere
    return "", 200

X-Webhook-Payload-Version é 2 em toda integração nova. A versão 1 é um formato antigo mantido para receptores existentes; se aparecer, o ID da tarefa fica em task_id, no nível mais alto.

Responda rápido

Qualquer 2xx em até 15 segundos conta como recebido. O resto — outro status, timeout, conexão recusada — vira nova tentativa. Confirme primeiro e faça o trabalho pesado (baixar, transcodificar, avisar usuários) num job em segundo plano.

Novas tentativas e duplicatas

Uma entrega que falha é tentada mais 7 vezes, depois de 10 s, 30 s, 2 min, 10 min, 30 min, 2 h e 6 h. Cada tentativa é assinada de novo com um timestamp novo, então compare sempre o timestamp com a hora atual, não com a primeira tentativa. Depois da última, a entrega aparece como falha no console da API, onde você pode reenviá-la manualmente.

Por isso a mesma tarefa pode chegar mais de uma vez. Deixe seu handler idempotente: guarde request_id com índice único e, ao vê-lo de novo, responda 200 sem fazer nada.

Se o seu servidor ficou fora do ar por mais tempo que a janela de tentativas, liste as tarefas recentes com GET /api/v1/jobs e leia as que ficaram para trás com recordInfo.

Endpoints da conta

Um endpoint da conta é uma URL cadastrada uma vez no console da API, junto com os eventos que ela quer receber. Ele recebe eventos de todas as tarefas da conta, então você não precisa mais passar callBackUrl.

EventoEnviado quando
task.completedAcontece qualquer um dos quatro abaixo. Assine este, a menos que precise diferenciá-los
task.succeededUma tarefa terminou com sucesso
task.failedUma tarefa falhou; a reserva foi liberada
task.expiredUma tarefa passou do prazo; a reserva foi liberada
task.canceledMantido apenas para registros antigos

As entregas seguem o formato Standard Webhooks, então bibliotecas prontas funcionam:

webhook-id: msg_01k3m8x9q2z4v7n5p6r8s0t1w3
webhook-timestamp: 1787045531
webhook-signature: v1,base64( HMAC-SHA256( key, "<webhook-id>.<webhook-timestamp>.<raw_body>" ) )

Duas diferenças em relação ao callback de tarefa: assina-se o próprio corpo (sem a etapa de SHA-256), e a chave do HMAC são os bytes obtidos ao decodificar em Base64 a parte do segredo depois de whsec_ — não a string em si. Durante uma rotação do segredo, o cabeçalho pode trazer duas assinaturas separadas por espaço; aceite a requisição se qualquer uma bater. webhook-id não muda entre tentativas: use-o para ignorar duplicatas. O data.object do evento é o registro da tarefa no momento em que o evento foi criado.

Nesta página