PopNowDocs da API
Guias
Conteúdo principal

Receitas

Receitas prontas para copiar e rodar em Python e Node.js para a API de imagem, vídeo e texto para fala do PopNow AI - imagens em lote a partir de um CSV, foto para vídeo, narrações linha por linha e um alerta no Slack quando um vídeo fica pronto.

Quatro scripts pequenos para as tarefas que as pessoas automatizam primeiro. Cada um está completo: instale requests (ou use Node 18+), defina POPNOW_API_KEY e rode. Todos compartilham os mesmos três auxiliares (chamar, esperar, salvar), então, depois de ler um, os outros são algumas linhas cada. A página de Receitas no popnow.ai monta os mesmos scripts para qualquer modelo que você escolher.

ReceitaModeloCusto por item
Imagens em lote a partir de um CSValibaba/z-image-turbo/text-to-image$0.01 por imagem
Foto para vídeominimax/h3/image-to-video$0.185 por 5 s em 768p
Uma narração por linhagoogle/gemini-3.8-flash-tts/text-to-speechcerca de $0.007 por 20 s de fala
Alerta no Slack quando um vídeo fica prontominimax/h3/image-to-video$0.185 por 5 s em 768p

Troque o modelo por qualquer outro em Modelos; mantenha os campos de input apenas nos que o schema daquele modelo lista.

Os auxiliares

Todas as receitas em Python terminam com estes. create envia uma tarefa com uma chave de idempotência, wait consulta com espera crescente até a tarefa ser final e os arquivos estarem copiados, save baixa o primeiro arquivo.

helpers.py
import os, time
import requests

BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}


def call(method, path, **kwargs):
    headers = {**HEADERS, **kwargs.pop("headers", {})}
    body = requests.request(method, BASE + path, headers=headers, timeout=60, **kwargs).json()
    if body["code"] != 200:
        raise RuntimeError(f'{body["code"]} {body["msg"]}')
    return body["data"]


def create(payload, key):
    return call("POST", "/jobs/createTask", json=payload, headers={"Idempotency-Key": key})["taskId"]


def wait(task_id):
    delay = 2.0
    while True:
        time.sleep(delay)
        task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
        assets = (task.get("output") or {}).get("assets") or []
        copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
        if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
            return task
        delay = min(delay * 1.5, 15.0)


def save(task, path):
    if task["state"] != "succeeded":
        raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
    url = task["output"]["assets"][0]["url"]
    with open(path, "wb") as f:
        f.write(requests.get(url, timeout=300).content)

Imagens em lote a partir de um CSV

prompts.csv tem uma coluna prompt; cada linha vira uma imagem em out/. Todas as tarefas são enviadas primeiro e coletadas depois, então são geradas em paralelo. A chave de idempotência é derivada da linha, então rodar de novo depois de uma falha nunca cobra duas vezes uma linha que já foi processada.

batch_images.py
import csv, hashlib, pathlib
from helpers import create, wait, save

MODEL = "alibaba/z-image-turbo/text-to-image"

out = pathlib.Path("out")
out.mkdir(exist_ok=True)
with open("prompts.csv", newline="", encoding="utf-8") as f:
    rows = [r["prompt"] for r in csv.DictReader(f) if r.get("prompt")]

tasks = []
for i, prompt in enumerate(rows, start=1):
    key = "batch-" + hashlib.sha256(f"{i}:{prompt}".encode()).hexdigest()[:32]
    tasks.append((i, create({"model": MODEL, "input": {"prompt": prompt, "aspect_ratio": "1:1"}}, key)))

for i, task_id in tasks:
    save(wait(task_id), out / f"{i:03d}.png")
    print("saved", i)

O mesmo em Node.js 18+, sem nenhum pacote:

batch_images.mjs
import { createHash } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';

const API = 'https://api.popnow.ai/api/v1';
const auth = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const call = async (path, init = {}) => {
  const body = await fetch(API + path, { ...init, headers: { ...auth, ...init.headers } }).then((r) => r.json());
  if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
  return body.data;
};

const prompts = (await readFile('prompts.txt', 'utf8')).split('\n').map((l) => l.trim()).filter(Boolean);
await mkdir('out', { recursive: true });

const ids = await Promise.all(prompts.map((prompt, i) => call('/jobs/createTask', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'batch-' + createHash('sha256').update(`${i}:${prompt}`).digest('hex').slice(0, 32) },
  body: JSON.stringify({ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt, aspect_ratio: '1:1' } }),
}).then((d) => d.taskId)));

for (const [i, id] of ids.entries()) {
  let task;
  do {
    await new Promise((r) => setTimeout(r, 3000));
    task = await call(`/jobs/recordInfo?taskId=${id}`);
  } while (task.state === 'queued' || task.state === 'running');
  if (task.state !== 'succeeded') { console.error(i, task.state, task.errorMessage); continue; }
  const file = await fetch(task.output.assets[0].url).then((r) => r.arrayBuffer());
  await writeFile(`out/${String(i + 1).padStart(3, '0')}.png`, Buffer.from(file));
}

Foto para vídeo

Envia uma foto local e depois pede a um modelo de imagem para vídeo que a dê vida. Rode assim: python photo_to_video.py photo.jpg.

photo_to_video.py
import mimetypes, sys, uuid
import requests
from helpers import call, create, wait, save

MODEL = "minimax/h3/image-to-video"


def upload(path):
    """Local file -> popnow:// reference you can put in any image, video or audio field."""
    data = open(path, "rb").read()
    kind = mimetypes.guess_type(path)[0] or "image/jpeg"
    ticket = call("POST", "/common/upload-url", json={"contentType": kind, "bytes": len(data)})
    requests.put(ticket["uploadUrl"], data=data, headers=ticket.get("headers", {}), timeout=120).raise_for_status()
    return call("POST", f'/files/{ticket["fileId"]}/commit')["uri"]


ref = upload(sys.argv[1])
task_input = {"image_url": ref, "prompt": "Gentle camera push-in, hair moving in the wind", "duration_seconds": 5, "resolution": "768p"}
task_id = create({"model": MODEL, "input": task_input}, str(uuid.uuid4()))
save(wait(task_id), "clip.mp4")
print("saved clip.mp4")

Teste a requisição sem código antes:

curl -X POST "https://api.popnow.ai/api/v1/jobs/createTask" \
  -H "Authorization: Bearer $POPNOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"model": "minimax/h3/image-to-video", "input": {"image_url": "https://media.popnow.ai/movie/lighthouse.webp", "prompt": "The waves roll in slowly", "duration_seconds": 5}}'

Uma narração por linha

lines.txt tem uma linha de roteiro por linha; cada uma vira o seu próprio arquivo de áudio em voice/, todos com a mesma voz. As opções de voice_id incluem Kore, Puck, Charon e Aoede; a página do modelo lista todas, junto com style e accent.

voiceover.py
import hashlib, pathlib
from helpers import create, wait, save

MODEL = "google/gemini-3.8-flash-tts/text-to-speech"

out = pathlib.Path("voice")
out.mkdir(exist_ok=True)
lines = [l.strip() for l in open("lines.txt", encoding="utf-8") if l.strip()]

tasks = []
for i, line in enumerate(lines, start=1):
    key = "voice-" + hashlib.sha256(f"{i}:{line}".encode()).hexdigest()[:32]
    tasks.append((i, create({"model": MODEL, "input": {"text": line, "voice_id": "Kore", "style": "empathetic"}}, key)))

for i, task_id in tasks:
    task = wait(task_id)
    mime = ((task.get("output") or {}).get("assets") or [{}])[0].get("mime", "")
    ext = {"audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/ogg": ".ogg"}.get(mime, ".audio")
    save(task, out / f"{i:03d}{ext}")
    print("saved", i)

Alerta no Slack quando um vídeo fica pronto

Em vez de consultar, passe callBackUrl ao criar a tarefa. Um pequeno servidor Express verifica a assinatura sobre o corpo bruto e publica o resultado num webhook de entrada do Slack. Defina POPNOW_WEBHOOK_SECRET (obtido no console da API) e SLACK_WEBHOOK_URL.

notify.mjs (npm install express)
import { createHash, createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
import express from 'express';

const BASE = 'https://api.popnow.ai/api/v1';

// 1. Create the task with a callBackUrl that points at this server.
export async function createVideo() {
  const res = await fetch(BASE + '/jobs/createTask', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': randomUUID(),
    },
    body: JSON.stringify({
      model: 'minimax/h3/image-to-video',
      input: { image_url: 'https://media.popnow.ai/movie/lighthouse.webp', prompt: 'The waves roll in slowly', duration_seconds: 5 },
      callBackUrl: 'https://your-server.example.com/hooks/popnow',
    }),
  });
  return (await res.json()).data.taskId;
}

// 2. Receive the finished task, check the signature on the raw body, then post to Slack.
const app = express();
app.post('/hooks/popnow', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestamp = req.header('X-Webhook-Timestamp');
  const signature = req.header('X-Webhook-Signature') ?? '';
  const event = JSON.parse(req.body.toString('utf8'));
  const task = event.data;
  const digest = createHash('sha256').update(req.body).digest('hex');
  const expected = createHmac('sha256', process.env.POPNOW_WEBHOOK_SECRET)
    .update(`${task.taskId}.${timestamp}.${digest}`)
    .digest('base64');
  const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
  res.status(200).end(); // answer first; deliveries are retried until you return 2xx

  const text = task.state === 'succeeded'
    ? `Video ready: ${task.output.assets[0].url}`
    : `Task ${task.taskId} ${task.state}: ${task.errorMessage ?? 'no detail'}`;
  await fetch(process.env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
});
app.listen(3000);

O link na mensagem do Slack é assinado e funciona por uns 20 minutos; publique também o taskId, caso as pessoas abram o link depois. Webhooks explica as novas tentativas e as entregas duplicadas.

Preços e limites

Cada receita gasta o saldo separado da API em USD pelo preço normal do modelo por item e mais nada; ler tarefas e enviar arquivos é de graça. Itens que falham não custam nada, e como cada item tem a sua própria chave de idempotência, rodar um script de novo só cria os itens que nunca foram processados. Antes de um lote grande, calcule o preço de um item com jobs/quote e defina um limite diário na chave (Autenticação). Enviar centenas de tarefas de uma vez é aceitável, desde que dentro dos limites de taxa.

Erros comuns

ErroPor quêCorreção
KeyError: 'POPNOW_API_KEY'A variável não está definida neste terminalexport POPNOW_API_KEY="sk-popnow-..."
RuntimeError: 400 … com um campo no nomeO modelo não aceita esse campo ou valorConfira a página do modelo; os campos variam por modelo
RuntimeError: 40201 …USD insuficientes na API para a próxima tarefaAdicione fundos em API billing; as tarefas já aceitas continuam rodando
RuntimeError: 429 …Requisições demais ao mesmo tempoEnvie em grupos menores e respeite o Retry-After
RuntimeError: failed: …Um item falhou ou foi recusadoNada foi cobrado por ele; corrija essa linha e rode de novo
RuntimeError: 40003 … no commitOs bytes enviados não batem com o ticketPeça um novo ticket e envie de novo
O webhook retorna 401A assinatura foi calculada sobre o JSON já parseado em vez dos bytes brutos, ou o segredo está erradoUse express.raw e o segredo do console da API

Existe um SDK oficial de Python ou de Node?

Existe, os dois são de código aberto no GitHub e publicados como popnow no PyPI e no npm: pip install popnow e npm install popnow. SDKs, CLI e ferramentas de código aberto traz os detalhes. Estas receitas usam apenas requests e o fetch nativo, para rodar sem nada extra instalado.

Quantas tarefas um lote pode rodar ao mesmo tempo?

Muitas: envie todas e depois colete. Fique dentro dos limites de taxa da página Limites de taxa e coloque um limite de gasto diário na chave que você usa para os lotes.

O que acontece se o meu script travar no meio de um lote?

Rode de novo. Cada linha usa a mesma chave de idempotência de antes, então as linhas que já tinham sido aceitas devolvem a tarefa original em vez de serem cobradas duas vezes.

Nesta página