PopNowAPI-Doku
Anleitungen
Hauptinhalt

Code-Rezepte

Direkt ausführbare Python- und Node.js-Rezepte für die PopNow API für KI-Bilder, Video und Text-to-Speech – Bildserien aus CSV-Dateien, Foto zu Video, zeilenweise Sprachaufnahmen und eine Slack-Nachricht, sobald ein Video fertig ist.

Vier kleine Skripte für die Aufgaben, die häufig zuerst automatisiert werden. Jedes ist vollständig: requests installieren (oder Node 18+ nutzen), POPNOW_API_KEY setzen, ausführen. Alle nutzen dieselben drei Hilfsfunktionen für Aufruf, Warten und Speichern. Hast du ein Rezept gelesen, bestehen die anderen nur noch aus wenigen eigenen Zeilen. Die Recipes-Seite auf popnow.ai erstellt dieselben Skripte für jedes Modell deiner Wahl.

RezeptModellKosten pro Element
Bildserien aus einer CSV-Dateialibaba/z-image-turbo/text-to-image$0.01 pro Bild
Foto zu Videominimax/h3/image-to-video$0.185 für 5 s bei 768p
Eine Sprachaufnahme pro Zeilegoogle/gemini-3.8-flash-tts/text-to-speechetwa $0.007 pro 20 s Sprache
Slack-Nachricht, sobald ein Video fertig istminimax/h3/image-to-video$0.185 für 5 s bei 768p

Du kannst jedes andere Modell unter Modelle einsetzen. Verwende nur die input-Felder, die im Schema dieses Modells stehen.

Die Hilfsfunktionen

Jedes Python-Rezept verwendet am Ende diese Funktionen. create sendet einen Task mit Idempotency-Key, wait fragt mit wachsenden Abständen ab, bis der Task einen Endzustand erreicht hat und seine Dateien kopiert wurden, und save lädt die erste Datei herunter.

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)

Bildserien aus einer CSV-Datei

prompts.csv enthält eine Spalte prompt; jede Zeile wird zu einem Bild in out/. Zuerst werden alle Tasks gesendet und danach die Ergebnisse abgeholt, sodass die Bilder parallel entstehen. Der Idempotency-Key wird aus der Zeile abgeleitet. Nach einem Absturz erneut zu starten kostet deshalb für bereits angenommene Zeilen nichts extra.

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)

Dasselbe mit Node.js 18+, ohne zusätzliche Pakete:

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 zu Video

Lädt ein lokales Foto hoch und lässt es von einem Bild-zu-Video-Modell animieren. Starte es mit 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")

Die Anfrage zuerst ohne eigenen Code testen:

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}}'

Eine Sprachaufnahme pro Zeile

lines.txt enthält pro Zeile einen Textabschnitt; jeder wird zu einer eigenen Audiodatei in voice/, alle mit derselben Stimme. Zu den Optionen für voice_id gehören Kore, Puck, Charon und Aoede. Die Modellseite führt alle Stimmen auf, ebenso style und 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)

Slack-Nachricht, sobald ein Video fertig ist

Statt zu pollen, übergib beim Erstellen des Tasks callBackUrl. Ein kleiner Express-Server prüft die Signatur des rohen Bodys und sendet das Ergebnis an einen eingehenden Slack-Webhook. Setze POPNOW_WEBHOOK_SECRET (aus der API-Konsole) und 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);

Der Link in der Slack-Nachricht ist signiert und gilt etwa 20 Minuten. Sende auch die taskId, falls jemand die Nachricht später öffnet. Unter Webhooks findest du mehr zu Wiederholungen und doppelten Zustellungen.

Preise und Limits

Jedes Rezept verbraucht das separate API-USD-Guthaben zum normalen Modellpreis pro Element, ohne Aufschlag. Tasks lesen und Dateien hochladen ist kostenlos. Fehlgeschlagene Elemente kosten nichts. Weil jedes Element einen eigenen Idempotency-Key hat, erstellt ein erneuter Skriptlauf nur die Elemente, die noch nicht angenommen wurden. Frag vor großen Serien den Preis eines Elements mit jobs/quote ab und setze ein Tageslimit für den Schlüssel (Authentifizierung). Du kannst Hunderte Tasks gleichzeitig senden, solange du die Ratenlimits einhältst.

Häufige Fehler

FehlerUrsacheLösung
KeyError: 'POPNOW_API_KEY'Die Variable ist in dieser Shell nicht gesetztexport POPNOW_API_KEY="sk-popnow-..."
RuntimeError: 400 … mit einem FeldnamenDas Modell unterstützt dieses Feld oder diesen Wert nichtPrüfe die Modellseite; die Felder unterscheiden sich je nach Modell
RuntimeError: 40201 …Nicht genug API-USD für den nächsten TaskUnter API-Abrechnung aufladen; bereits angenommene Tasks laufen weiter
RuntimeError: 429 …Zu viele gleichzeitige AnfragenIn kleineren Gruppen senden und Retry-After beachten
RuntimeError: failed: …Ein Element ist fehlgeschlagen oder wurde abgelehntDafür wurde nichts berechnet; korrigiere die Zeile und starte erneut
RuntimeError: 40003 … beim CommitDie hochgeladenen Bytes passen nicht zum TicketEin neues Ticket anfordern und erneut hochladen
Webhook liefert 401Signatur aus geparstem JSON statt aus rohen Bytes berechnet oder falsches Secretexpress.raw und das Secret aus der API-Konsole verwenden

Gibt es ein offizielles Python- oder Node-SDK?

Ja, beide sind Open Source auf GitHub und als popnow auf PyPI und npm veröffentlicht: pip install popnow und npm install popnow. Details stehen unter SDKs, CLI und Open-Source-Tools. Diese Rezepte nutzen requests und das eingebaute fetch, damit sie ohne weitere Installationen laufen.

Wie viele Tasks kann eine Serie gleichzeitig ausführen?

Viele: Sende zuerst alle und hole danach die Ergebnisse ab. Halte die Ratenlimits auf der Seite zu Ratenlimits ein und setze für den Schlüssel, mit dem du Serien ausführst, ein tägliches Ausgabelimit.

Was passiert, wenn mein Skript mitten in einer Serie abstürzt?

Starte es erneut. Jede Zeile verwendet denselben Idempotency-Key wie zuvor. Bereits angenommene Zeilen liefern deshalb ihren ursprünglichen Task zurück, statt doppelt berechnet zu werden.

Auf dieser Seite