popnow is a typed Python client for the PopNow API that turns text, images and audio into AI videos, images, music and speech, using the same models, prices and credits as popnow.ai.
PopNow is a browser-based AI studio for videos, images and music: image-to-video, text-to-video, AI images, music and speech, one-tap trending video effects and image styles. This SDK wraps its developer API (https://api.popnow.ai/api/v1) so you can call Kling, Seedance, Wan, Vidu, MiniMax, Nano Banana, GPT Image, Flux, Suno and more from Python. Credit packs, no subscription; failed runs are refunded. Make it pop.
- Sync and async clients (
PopNow,AsyncPopNow) built onhttpx, the only dependency. - Typed results (
py.typed) with snake_case attributes, plus the raw JSON on every object. - One-line helpers:
client.images.generate(),client.videos.generate(),client.audio.speech(). - Safe retries: automatic
Idempotency-Key, back-off on429/500/503honouringRetry-After. - Local file uploads, asset library, task history, usage, balance and webhook signature verification.
1. Install (Python 3.9+):
pip install popnow2. Create an API key in the API console and export it:
export POPNOW_API_KEY="sk-popnow-..."3. Generate:
from popnow import PopNow
client = PopNow() # reads POPNOW_API_KEY
task = client.images.generate(
model="alibaba/z-image-turbo/text-to-image",
prompt="A small red fox curled up in a teacup, soft morning light",
)
print(task.state, task.credits, task.urls[0])
client.files.download(task.assets[0], "fox.png")That image costs a couple of credits (1 credit = $0.01). Buy credits on the pricing page; API calls and the website share one wallet.
| Area | SDK | API endpoint |
|---|---|---|
| Models and prices | client.models.list(), client.models.get(id) |
GET /api/v1/models, GET /api/v1/models/{model} |
| Price a request | client.jobs.quote() |
POST /api/v1/jobs/quote |
| Create a task | client.jobs.create() |
POST /api/v1/jobs/createTask |
| Read / wait | client.jobs.get(), client.jobs.wait() |
GET /api/v1/jobs/recordInfo |
| Task history | client.jobs.list(), client.jobs.iter() |
GET /api/v1/jobs |
| Retry / purge | client.jobs.retry(), client.jobs.purge() |
POST /api/v1/jobs/retry, POST /api/v1/jobs/purge |
| Download link | client.jobs.download_url() |
POST /api/v1/common/download-url |
| Upload a file | client.files.upload(path) |
POST /api/v1/common/upload-url, PUT, POST /api/v1/files/{id}/commit |
| Import a LoRA | client.files.import_lora(url) |
POST /api/v1/files/import |
| Asset library | client.assets.* |
/api/v1/assets* |
| Balance / usage | client.account.balance(), client.account.usage() |
GET /api/v1/chat/credit, GET /api/v1/usage |
| Webhooks | popnow.webhooks.verify_task_callback() |
X-Webhook-Signature header |
Model ids look like <maker>/<model>/<task>. Some examples; the live list with prices is client.models.list() or popnow.ai/docs/models:
| Kind | Example model ids |
|---|---|
| Text-to-image | alibaba/z-image-turbo/text-to-image, google/nano-banana-2.1/text-to-image, openai/gpt-image-2/text-to-image, black-forest-labs/flux-2-pro/text-to-image, bytedance/seedream-5.0-pro/text-to-image |
| Image editing | google/nano-banana-2.1/edit, openai/gpt-image-2/edit, popnow/background-remover-v1/edit |
| Image-to-video | alibaba/wan-3.0/image-to-video, kling/3.0/image-to-video, bytedance/seedance-2.0/image-to-video, vidu/q3/image-to-video, minimax/h3/image-to-video |
| Text-to-video | kling/3.0/text-to-video, alibaba/wan-3.0/text-to-video, bytedance/seedance-2.0/text-to-video |
| Music | suno/v6/text-to-audio, minimax/music-3.0/text-to-audio |
| Speech | google/gemini-3.8-flash-tts/text-to-speech, minimax/speech-2.8-hd/text-to-speech |
| Transcription | xai/grok-stt/speech-to-text |
| Upscaling | popnow/image-upscaler-v1/upscale, popnow/video-upscaler-v1/upscale |
Every model has its own input_schema (client.models.get(id).input_schema). Unknown fields are rejected with a 400, so typos fail fast.
import pathlib
from popnow import PopNow
client = PopNow()
task = client.videos.generate(
model="alibaba/wan-3.0/image-to-video",
image_url=pathlib.Path("photo.jpg"), # a Path is uploaded for you; URLs work too
prompt="Waves roll in slowly, the camera drifts toward the lighthouse",
resolution="480p",
duration_seconds=5,
timeout=1800,
)
client.files.download(task.assets[0], "clip.mp4")song = client.audio.music(model="suno/v6/text-to-audio", prompt="Upbeat lo-fi beat for a cooking video", instrumental=True)
voice = client.audio.speech(model="google/gemini-3.8-flash-tts/text-to-speech", text="Welcome back!", voice_id="Kore")
transcript = client.audio.transcribe(model="xai/grok-stt/speech-to-text", audio_url="https://example.com/interview.mp3").textThe helpers fill the shared field names (prompt, text, image_url, image_urls, video_url, audio_url, duration_seconds, resolution, aspect_ratio) and pass every other keyword straight into the model input. They wait for the result and raise TaskFailedError unless the task succeeds.
from popnow import PopNow, new_idempotency_key
client = PopNow()
body = {
"model": "alibaba/wan-3.0/image-to-video",
"input": {"image_url": "https://media.popnow.ai/movie/lighthouse.webp", "resolution": "720p", "duration_seconds": 5},
}
quote = client.jobs.quote(**body) # nothing is created or held
created = client.jobs.create(
**body,
idempotency_key=new_idempotency_key(), # reuse it on every retry of this work
quote_id=quote.quote_id,
expected_credits=quote.max_credits, # QuoteMismatchError (40901) if the price changed
callback_url="https://your-app.example.com/hooks/popnow",
)
task = client.jobs.wait(created, timeout=1800) # polls with back-off: 2s, 3s, 4.5s ... up to 15s
if task.is_succeeded:
print(task.urls)
else:
print(task.state, task.error_code, task.error_message)jobs.create(..., wait=60)holds the connection up to 60 seconds; if the task finishes in time,created.taskis the full record.jobs.create_and_wait(model=..., input=...)combines the server-side wait with polling.- Task states:
queued,running, thensucceeded,failedorexpired. Unknown states count as not final. output.assets[].urlis a signed link valid for about 20 minutes. Store thetask_id, not the link.
uploaded = client.files.upload("reference.png") # popnow://f/fil_...
asset = client.assets.save_upload(uploaded.file_id, name="Product shot") # popnow://a/ast_...
for a in client.assets.iter(kind="image", sort="recent"):
print(a.name, a.uri)
lora = client.files.import_lora("your-name/your-lora-repo", token="hf_...") # for loras[].pathUploads are temporary; saved assets stay until you delete them. Images up to 10 MiB, video and audio up to 90 MiB (600 seconds), LoRA .safetensors up to 2 GiB.
import asyncio
from popnow import AsyncPopNow
async def main() -> None:
async with AsyncPopNow() as client:
balance = await client.account.balance()
task = await client.images.generate(model="alibaba/z-image-turbo/text-to-image", prompt="A paper boat")
print(balance.available_credits, task.urls)
asyncio.run(main())import os
from popnow import webhooks, WebhookVerificationError
def handle(raw_body: bytes, headers: dict) -> int:
try:
event = webhooks.verify_task_callback(raw_body, headers, os.environ["POPNOW_WEBHOOK_SECRET"])
except WebhookVerificationError:
return 401
task = webhooks.task_from_event(event) # event["request_id"] is the stable delivery id
print(task.task_id, task.state)
return 200Task callbacks are signed as base64(HMAC-SHA256(secret, "<taskId>.<timestamp>.<sha256_hex(raw_body)>")); timestamps more than five minutes away are rejected. Account endpoints use the Standard Webhooks format: webhooks.verify_endpoint_event(raw_body, headers, "whsec_..."). Always verify the raw request bytes.
client = PopNow(
api_key="sk-popnow-...", # default: $POPNOW_API_KEY
base_url="https://api.popnow.ai", # default: $POPNOW_BASE_URL or this
timeout=60.0, # seconds per request, or an httpx.Timeout
max_retries=2, # network errors and codes 429, 500, 503, 50301
language="de", # Accept-Language for error messages
http_client=None, # bring your own httpx.Client (proxies, mounts)
)Keep keys on your server; never ship them in a browser or mobile app.
Every error response is {"code", "msg", "request_id"}. The SDK raises a typed exception chosen by the business code:
| Exception | code |
|---|---|
BadRequestError |
400, 40003, 40004 |
AuthenticationError |
401 |
InsufficientCreditsError / SpendCapExceededError (both PaymentRequiredError) |
40201 / 40202 |
PermissionDeniedError |
403, 40301, 40302, 40303, 40310 |
NotFoundError |
404 |
ConflictError / QuoteMismatchError |
409 / 40901 |
PayloadTooLargeError |
413 |
RateLimitError |
429 (see err.retry_after) |
InternalServerError |
500 |
ServiceUnavailableError / ModelUnavailableError |
503, 50302 / 50301 |
APIConnectionError / APITimeoutError |
no HTTP response |
TaskFailedError |
task ended failed or expired (see err.error_code) |
WaitTimeoutError |
the local wait budget ran out; the task keeps running |
All API errors carry code, status_code, message, request_id and, for some asset and content errors, reason (data.reason).
from popnow import APIError, InsufficientCreditsError, RateLimitError
try:
client.jobs.create(model="kling/3.0/text-to-video", input={"prompt": "A koi pond in the rain"})
except InsufficientCreditsError:
print("Top up at https://popnow.ai/pricing")
except RateLimitError as err:
print("retry in", err.retry_after, "seconds")
except APIError as err:
print(err.code, err.message, err.request_id, err.reason)Branch on code (and task.error_code), never on message text. Quote request_id when contacting support.
See examples/: generate_image.py, image_to_video.py, text_to_speech.py, quote_and_create.py, async_batch.py, save_to_library.py, webhook_receiver.py.
Call client.videos.generate(model="alibaba/wan-3.0/image-to-video", image_url=..., prompt=...). Pass a public HTTPS URL, a popnow:// URI, or a pathlib.Path to a local photo, which the SDK uploads first. The call waits for the clip and returns the task with a signed download URL.
The API uses the same credits and prices as popnow.ai: 1 credit = $0.01, bought in credit packs with no subscription. Each model lists its price in client.models.get(id).credits, and client.jobs.quote() returns the exact amount for one request. Failed and expired tasks release their hold, so they cost nothing.
Send the same Idempotency-Key with every retry of one piece of work. The SDK generates one per call and reuses it across its own automatic retries; pass idempotency_key= yourself (for example order-8814-render-1) to stay safe across process restarts. Keys are remembered for 24 hours.
No. jobs.wait() polls with back-off for you, jobs.create(wait=60) returns quick results in the same call, and in production you can pass callback_url and verify the webhook with popnow.webhooks.verify_task_callback().
client.files.upload("photo.jpg") asks for an upload ticket, PUTs the bytes and commits them, then returns a uri (popnow://f/fil_...) for image_url or image_urls. To keep a file for reuse, save it with client.assets.save_upload().
Kling, Seedance, Wan, Vidu, MiniMax, Nano Banana, GPT Image, Flux, Seedream, Suno and more. Call client.models.list(modality="video") for the live list with availability and prices.
Yes. AsyncPopNow has the same resources and helpers as PopNow, with await. Combine it with asyncio.gather to run many tasks at once within the rate limits.
Yes. PopNow runs a remote MCP server at https://api.popnow.ai/mcp with tools for images, video, music, speech and effects. See popnow.ai/mcp.
- Website: popnow.ai
- Docs: popnow.ai/docs
- API reference (OpenAPI): popnow.ai/docs/openapi.yaml
- Pricing: popnow.ai/pricing
- MCP server: popnow.ai/mcp
- llms.txt: popnow.ai/llms.txt
- Other PopNow repos: popnow-mcp, popnow-skills, popnow-js, popnow-api, popnow-blender
Issues and pull requests are welcome; see CONTRIBUTING.md. To report a vulnerability, see SECURITY.md.
MIT © PopNow