# PopNow API Documentation — Full English Corpus > Generated from the same MDX sources as the public documentation, in sidebar order. Prefer the OpenAPI contract for exact request and response shapes, and the model reference pages for each model's parameters and prices. - Native task API: https://api.popnow.ai/api/v1 (authentication: Authorization: Bearer sk-popnow-... in the header) - Create a task: POST https://api.popnow.ai/api/v1/jobs/createTask; read it: GET https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=...; price it first: POST https://api.popnow.ai/api/v1/jobs/quote - Callable models with schemas and credit prices: GET https://api.popnow.ai/api/v1/models and GET https://api.popnow.ai/api/v1/models/{model} - OpenAI Videos compatible endpoints: https://api.popnow.ai/v1/videos (video generation only; no chat or text endpoints are offered) - OpenAPI 3.1 contract: https://popnow.ai/docs/openapi.yaml - Short index: https://popnow.ai/docs/llms.txt - Full English corpus: https://popnow.ai/docs/llms-full.txt - Markdown of any page: https://popnow.ai/docs/markdown/ (other languages: https://popnow.ai/docs//markdown/) - Billing: credits, 1 credit = US$0.01, the same wallet as the PopNow website; failed tasks are not charged - Contact support: https://popnow.ai/contact or support@popnow.ai (include the request_id or task ID; never send an API key) - API keys, usage and webhook endpoints: https://popnow.ai/api/console --- # Quickstart Canonical: https://popnow.ai/docs/quickstart > Create an API key, send your first request and download the result in about five minutes. This page takes you from nothing to a generated image with one key and one request. Everything after that — video, audio, webhooks, uploads — is the same request with a different `model` and `input`. ## Before you start [#before-you-start] * **A PopNow account.** Sign up at [popnow.ai](https://popnow.ai). The free credits you get at sign-up work with the API too; when you need more, buy a [credit pack](https://popnow.ai/pricing). There is no separate API plan. * **A terminal with `curl`.** The examples use bash. On Windows, use WSL or Git Bash. ## 1. Create an API key [#create-a-key] Open the [API console](https://popnow.ai/api/console/keys) and choose **Create key**. Copy the key straight away: the full value is shown only once. Then put it in an environment variable so it never lands in your code: ```bash export POPNOW_API_KEY="sk-popnow-..." ``` Every key starts with `sk-popnow-` and spends the credits in your account. Keep it on your server; [Authentication](https://popnow.ai/docs/authentication) explains restrictions, rotation and what to do if a key leaks. ## 2. Send a task [#send-a-task] Paste this into the same terminal. It asks a fast image model for one picture and costs a couple of credits: ```bash export POPNOW_API_KEY="sk-popnow-..." # your key from the API console curl -X POST "https://api.popnow.ai/api/v1/jobs/createTask?wait=60" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "model": "alibaba/z-image-turbo/text-to-image", "input": { "prompt": "A small red fox curled up in a teacup, soft morning light, studio photo" } }' ``` What each part does: * **`model`** is the model ID — which model runs and which job it does. Every model's ID is on [Models](https://popnow.ai/docs/models). * **`input`** is checked against that model's own input schema. Unknown fields are rejected, so typos fail fast instead of being ignored. * **`Idempotency-Key`** makes a retry safe: send the same key again after a timeout and you get the original task back instead of a second charge. See [Idempotency](https://popnow.ai/docs/idempotency). * **`?wait=60`** keeps the connection open for up to 60 seconds, so a quick image comes back in this same response. ## 3. Read the result [#read-the-result] A finished task looks like this: ```json { "code": 200, "msg": "success", "data": { "taskId": "job_01JH8Z4Q5W2N7R3K9M6P1C0VXD", "model": "alibaba/z-image-turbo/text-to-image", "state": "succeeded", "output": { "assets": [ { "url": "https://…signed-link…", "expiresAt": "2026-10-16T09:20:00Z", "mime": "image/png", "width": 1024, "height": 1024 } ] }, "credits": 2, "cost": "0.02", "settled": true }, "request_id": "req_…" } ``` * `state` is `succeeded`. A task that could not finish says `failed` and carries an `errorCode`; see [Errors](https://popnow.ai/docs/errors#task-failure-codes). * `output.assets[0].url` is a signed link to the file. It works for about 20 minutes, needs no API key, and you can always get a fresh one by reading the task again. * `credits` is what the task was charged, in whole credits (1 credit = $0.01). `cost` carries the same amount as a US dollar string. If the model needs more than 60 seconds — every video model does — the response is a `202` with `state: "queued"` and a `taskId` instead. Ask for the task until it is final: ```bash curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=job_..." \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` Check every few seconds until `state` is `succeeded`, `failed` or `expired`. In production, pass a `callBackUrl` and let a [webhook](https://popnow.ai/docs/webhooks) tell you instead. ## The same request in code [#in-code] ```javascript title="JavaScript (Node.js 18+)" const API = 'https://api.popnow.ai/api/v1'; const headers = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}`, 'Content-Type': 'application/json', }; const created = await fetch(`${API}/jobs/createTask`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A small red fox curled up in a teacup, soft morning light' }, }), }).then((r) => r.json()); if (created.code !== 200) throw new Error(`${created.code} ${created.msg}`); // Poll until the task reaches a final state. let task; do { await new Promise((r) => setTimeout(r, 3000)); const res = await fetch(`${API}/jobs/recordInfo?taskId=${created.data.taskId}`, { headers }); task = (await res.json()).data; } while (task.state === 'queued' || task.state === 'running'); console.log(task.state, task.output?.assets?.[0]?.url); ``` ```python title="Python (requests)" import os, time, uuid, requests API = "https://api.popnow.ai/api/v1" headers = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"} created = requests.post( f"{API}/jobs/createTask", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={ "model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "A small red fox curled up in a teacup, soft morning light"}, }, timeout=30, ).json() if created["code"] != 200: raise RuntimeError(f'{created["code"]} {created["msg"]}') # Poll until the task reaches a final state. while True: time.sleep(3) task = requests.get( f"{API}/jobs/recordInfo", headers=headers, params={"taskId": created["data"]["taskId"]}, timeout=30, ).json()["data"] if task["state"] not in ("queued", "running"): break print(task["state"], task.get("output", {}).get("assets", [{}])[0].get("url")) ``` ## The response envelope [#envelope] Every JSON response has the same four fields: ```json { "code": 200, "msg": "success", "data": { }, "request_id": "req_…" } ``` `code` is a business code, not an HTTP status: `200` means success, anything else is an error listed on [Errors](https://popnow.ai/docs/errors). Branch on `code`, never on the wording of `msg`. Quote `request_id` when you contact support. ## Next [#next] - [Pick a model](https://popnow.ai/docs/models): Every model ID, its parameters and its price in credits. - [How tasks work](https://popnow.ai/docs/tasks): States, polling, wait, retries and the task list. - [Upload your own files](https://popnow.ai/docs/media): Send reference images, video and audio. - [Get notified](https://popnow.ai/docs/webhooks): Receive the result instead of polling. --- # How the PopNow API works Canonical: https://popnow.ai/docs/overview > What the API can make, the five ideas every other page builds on, and the order to read things in. The PopNow API lets your own software use the image, video and audio models you know from [popnow.ai](https://popnow.ai). Same models, same prices, same credits — just called from code instead of clicked in a browser. ## What you can make [#what-you-can-make] | Output | Examples | | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | | Images | Text to image, image editing with one or more references, upscaling, background removal, layer separation | | Video | Text to video, image to video, first-and-last-frame video, reference video, extending and editing clips, lip sync, talking avatars | | Audio | Music, sound effects, text to speech, a soundtrack for an existing video, speech to text | The full list, with each model's ID and price, is on [Models](https://popnow.ai/docs/models). The API covers image, video and audio generation only; chat and text models are not part of it. ## Five ideas you will meet everywhere [#core-concepts] ### Model ID [#model-id] Each thing you can call has an ID in the form `//`, for example `alibaba/wan-3.0/image-to-video` or `google/nano-banana-2.1/edit`. You put it in the `model` field of every request. Image editing always ends in `/edit`. Tools built by PopNow itself, such as background removal, use `popnow` as the maker. ### Task [#task] Generation takes from a few seconds to several minutes, so every request creates a **task**. You get a `taskId` at once and read the result when it is ready. A task moves through `queued` → `running` → one final state: `succeeded`, `failed` or `expired`. Details: [Tasks](https://popnow.ai/docs/tasks). ### Credits [#credits] The API spends the same credits as the website. One credit is $0.01 at the base rate. When a task is accepted, its highest possible price is held; when it finishes, you pay the real amount (never more than the hold) and the rest is released. A task that fails costs nothing. Details: [Billing](https://popnow.ai/docs/billing). ### Idempotency key [#idempotency-key] A network timeout does not tell you whether the server received your request. Send an `Idempotency-Key` header with every task you create, and a retry with the same key returns the original task instead of making — and charging — a second one. Details: [Idempotency](https://popnow.ai/docs/idempotency). ### Webhook [#webhook] Instead of asking for a task again and again, give it a `callBackUrl`. When the task finishes, PopNow sends the full result to that URL, signed so you can check it came from us. Details: [Webhooks](https://popnow.ai/docs/webhooks). ## What a request looks like [#request-shape] ```text POST https://api.popnow.ai/api/v1/jobs/createTask Authorization: Bearer sk-popnow-... Idempotency-Key: 7d0c9a52-... { "model": "alibaba/wan-3.0/image-to-video", "input": { "image_url": "https://…/photo.jpg", "prompt": "The waves roll in slowly" }, "callBackUrl": "https://your-app.example.com/hooks/popnow" } ``` Every response uses one envelope, `{ "code", "msg", "data", "request_id" }`, where `code: 200` means success. ## Read in this order [#reading-order] 1. [Quickstart](https://popnow.ai/docs/quickstart) — your first result in five minutes. 2. [Authentication](https://popnow.ai/docs/authentication) — how to keep a key safe and restrict what it can do. 3. [Models](https://popnow.ai/docs/models) — pick a model and read its parameters. 4. [Tasks](https://popnow.ai/docs/tasks) and [Webhooks](https://popnow.ai/docs/webhooks) — the full lifecycle. 5. [Media](https://popnow.ai/docs/media) — send your own images, video and audio. 6. [Errors](https://popnow.ai/docs/errors) and [Rate limits](https://popnow.ai/docs/limits) — before you go live. Want to try a model before writing code? Every model has a Playground on its API page on popnow\.ai, which runs on your account with no key needed. --- # Authentication Canonical: https://popnow.ai/docs/authentication > The key format, where to keep a key, per-key restrictions, rotation and what to do if a key leaks. Every request to the API carries one API key in the `Authorization` header: ```http Authorization: Bearer sk-popnow-3f9c0a… ``` There is no other way to authenticate: keys in the URL query string are never accepted, and your popnow\.ai login does not work on the API. One exception: the [OpenAI-compatible routes](https://popnow.ai/docs/openai-videos) under `/v1` also read the key from an `x-api-key` header, so OpenAI-style clients work unchanged. The native `/api/v1` routes only read `Authorization`. ## What a key looks like [#key-format] A key is `sk-popnow-` followed by 48 lowercase hexadecimal characters. The API console only ever shows the first few characters after you create it, so a key you lose cannot be recovered — create a new one instead. ## Create a key [#create-a-key] 1. Sign in at [popnow.ai](https://popnow.ai) and open the [API console](https://popnow.ai/api/console/keys). 2. Choose **Create key** and give it a name that says where it will live, such as `production-backend`. 3. Copy the key immediately. It is shown once. You can hold several keys at once — one per environment or per service is a good habit, because you can then revoke one without touching the others. All keys spend from the same credit balance. ## Where to keep it [#where-to-keep-it] * In an environment variable or your platform's secret store (`POPNOW_API_KEY`), not in source code. * Never in a browser, a mobile app or any other code that runs on someone else's device. Anyone who can read the key can spend your credits. Put your own server in between and let it call PopNow. * Not in logs. If you log request headers, drop `Authorization` first. ## Restrict what a key can do [#restrictions] Each key can carry its own limits, set in the API console: | Restriction | What it does | Error when it blocks a request | | -------------- | ----------------------------------------------------------------- | ------------------------------ | | Allowed models | The key can call only the models you list | `40301` | | IP allowlist | Requests must come from these addresses or ranges | `40302` | | Spend caps | Daily, monthly and lifetime limits for this key, shown in credits | `40202` | By default a key has no restrictions and no caps. Give production keys the narrowest set that still works. ## Tasks belong to the key that created them [#task-scope] `recordInfo`, `retry`, `purge`, the task list and usage only see tasks created **with the same key**. A task created with another key on your account answers `404`, exactly like a task that does not exist. Generations you start on the website are not visible to any key either. For an account-wide view, use the API console. ## Rotation [#rotation] To replace a key without downtime: 1. Create the new key. 2. Deploy it everywhere the old one was used. 3. Watch the old key's last-used time in the console stop moving. 4. Revoke the old key. ## If a key leaks [#after-a-leak] 1. **Revoke it now** in the API console. Revocation takes effect on the next request. 2. **Create a replacement** and deploy it. 3. **Check what it was used for**: the console's logs and usage show every task the key created. 4. **Tighten the replacement** with an IP allowlist and a spend cap. Contact [support](https://popnow.ai/contact) if you see spending you did not make. ## What a failure looks like [#errors] All three authentication problems return HTTP `401` with `code: 401`; `msg` tells them apart: | `msg` says | What happened | What to do | | ---------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Missing credentials | No `Authorization` header arrived | Check that the header is set and that the environment variable is loaded in the running process | | API key format is not recognized | Something arrived, but it is not a PopNow key | Make sure it starts with `sk-popnow-`; you may have pasted a different value | | Credentials are invalid or expired | The key does not exist, was revoked or has expired | Create a new key; retrying will never succeed | A valid key can still be refused with `403` — for example when the account's email address is not verified yet (`40310`). See [Errors](https://popnow.ai/docs/errors#permissions). --- # Models Canonical: https://popnow.ai/docs/models > Every image, video and audio model you can call, how model IDs are built, and how to read prices and input schemas from code. Each model you can call has a **model ID** that goes in the `model` field of [createTask](https://popnow.ai/docs/tasks). The table at the bottom of this page lists every one of them with its starting price; each name links to a reference page with the full parameter list, price tiers and a request you can copy. ## How a model ID is built [#model-id] ```text // alibaba/wan-3.0/image-to-video google/nano-banana-2.1/edit popnow/background-remover-v1/edit ``` * **maker** is who built the model. Tools built by PopNow itself use `popnow`. * **model** keeps its version dots: `seedance-2.0`, not `seedance-2-0`. * **task** says what the call does: `text-to-image`, `edit`, `image-to-video`, `reference-to-video`, `video-extend`, `text-to-speech`, `text-to-audio` and so on. Image editing is always `edit`. A model family usually has several IDs, one per task. Pick the one that matches what you send: `.../text-to-video` takes only a prompt, `.../image-to-video` needs an opening image. ## Read the catalog from code [#catalog-api] The live catalog is the source of truth: it includes availability right now and the prices that apply to your account. ```bash # Every video model, with input schemas curl "https://api.popnow.ai/api/v1/models?modality=video&includeSchema=1" \ -H "Authorization: Bearer $POPNOW_API_KEY" # One model. The ID contains slashes; send it as it is. curl "https://api.popnow.ai/api/v1/models/alibaba/wan-3.0/image-to-video" \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` Filters on the list: `modality` (`image`, `video`, `audio`), `provider`, `task`, `search`, plus `includeSchema=1` and `includeExamples=1` to add input schemas and validated example inputs. If your HTTP client treats a path parameter as one segment, encode the slashes as `%2F`. ### Fields worth reading [#fields] | Field | Use it for | | -------------------------- | ------------------------------------------------------------------------------ | | `model` | The ID to send. Older spellings keep working, but always store this one | | `displayName`, `provider` | What to show your users | | `modality`, `tasks` | Grouping and filtering | | `available` | `false` means the model cannot take tasks right now | | `credits` | The price in credits, see below | | `inputSchema` | The JSON Schema that `input` is validated against | | `taskTimeoutSeconds` | How long a task may run before it is stopped and released | | `maxOutputDurationSeconds` | The longest clip the model can return, when it has a length setting | | `version` | Changes whenever the schema or metadata change; re-read the model when it does | ### Reading prices [#pricing] `credits` is the same price popnow\.ai shows, in credits (1 credit = $0.01): ```json "credits": { "unit": "per_second", "perUnit": 6.75, "rules": [ { "when": { "*": "480p" }, "perUnit": 6.75 }, { "when": { "*": "720p" }, "perUnit": 13.5 }, { "when": { "*": "1080p" }, "perUnit": 27 } ] } ``` `unit` says how the price scales (`per_image`, `per_second`, `per_request`, `per_1k_characters`), `perUnit` is the starting price, and each rule applies when the request matches its `when` values (`*` stands for the setting that decides the tier, here `resolution`). A task's final charge is rounded up to whole credits. For the exact amount of one specific request, ask [jobs/quote](https://popnow.ai/docs/billing#quote) with the same body you will send to `createTask`. ## Input schemas [#input-schema] `inputSchema` is standard JSON Schema (draft 2020-12). A few rules hold for every model: * **Unknown fields are rejected.** A misspelt `aspect_ration` returns `400` instead of being silently ignored. * **Field names are shared across models.** The same idea always has the same name: `prompt`, `negative_prompt`, `image_url`, `image_urls`, `video_url`, `audio_url`, `duration_seconds`, `aspect_ratio`, `resolution`, `seed`, `generate_audio`. * **Defaults are filled in for you.** Send only what you want to change. * **Media fields** take a public HTTPS URL, a `popnow://` file you uploaded, or a saved asset. See [Media](https://popnow.ai/docs/media). * **Display hints** live in an `x-ui` object on each property (labels, order, slider steps). They never change validation; ignore keys you do not recognise. If you leave out `seed` on a model that has one, a random seed is chosen and returned in the task's `input`, so you can repeat a result you like. ## Caching [#caching] Catalog responses are private to your account and cacheable for 60 seconds. A sensible pattern is to cache the list for a few minutes, re-read a single model when its `version` changes, and treat `available: false` as "try again later or pick another model". ## Every model you can call [#all-models] Prices are the lowest tier per unit; higher resolutions, longer clips and extra options cost more. Each model's page shows every tier. ### Image (65) | Model | Model IDs | From | | --- | --- | --- | | [Nano Banana 2.1](https://popnow.ai/docs/models/nano-banana-2-1) | `google/nano-banana-2.1/text-to-image`
`google/nano-banana-2.1/edit` | 7.2 credits per image ($0.072) | | [Nano Banana Pro](https://popnow.ai/docs/models/nano-banana-pro) | `google/nano-banana-pro/text-to-image`
`google/nano-banana-pro/edit` | 16.08 credits per image ($0.1608) | | [GPT Image 2.5 Sunburst](https://popnow.ai/docs/models/gpt-image-2-5-sunburst) | `openai/gpt-image-2.5-sunburst/text-to-image`
`openai/gpt-image-2.5-sunburst/edit` | 0.27 credits per image ($0.0027) | | [Seedream 5.0 Pro](https://popnow.ai/docs/models/seedream-5-0-pro) | `bytedance/seedream-5.0-pro/text-to-image`
`bytedance/seedream-5.0-pro/edit`
`bytedance/seedream-5.0-pro/layer-decomposition` | 5.4 credits per image ($0.054) | | [FLUX.2 Max](https://popnow.ai/docs/models/flux-2-max) | `black-forest-labs/flux-2-max/text-to-image`
`black-forest-labs/flux-2-max/edit` | 10.5 credits per image ($0.105) | | [Qwen Image 2.1 Pro](https://popnow.ai/docs/models/qwen-image-2-1-pro) | `alibaba/qwen-image-2.1-pro/text-to-image`
`alibaba/qwen-image-2.1-pro/edit` | 5.4 credits per image ($0.054) | | [Qwen Image 2.1 Turbo](https://popnow.ai/docs/models/qwen-image-2-1-turbo) | `alibaba/qwen-image-2.1-turbo/text-to-image`
`alibaba/qwen-image-2.1-turbo/edit` | 2.16 credits per image ($0.0216) | | [Qwen Image 2.1](https://popnow.ai/docs/models/qwen-image-2-1) | `alibaba/qwen-image-2.1/text-to-image`
`alibaba/qwen-image-2.1/edit` | 3.6 credits per image ($0.036) | | [Qwen Image 2.1 LoRA](https://popnow.ai/docs/models/qwen-image-2-1-lora) | `alibaba/qwen-image-2.1-lora/text-to-image`
`alibaba/qwen-image-2.1-lora/edit` | 4.5 credits per image ($0.045) | | [Seedream 5.0 Flash](https://popnow.ai/docs/models/seedream-5-0-flash) | `bytedance/seedream-5.0-flash/text-to-image`
`bytedance/seedream-5.0-flash/edit`
`bytedance/seedream-5.0-flash/layer-decomposition` | 3.9 credits per image ($0.039) | | [GPT Image 2.5 Flare](https://popnow.ai/docs/models/gpt-image-2-5-flare) | `openai/gpt-image-2.5-flare/text-to-image`
`openai/gpt-image-2.5-flare/edit` | 0.27 credits per image ($0.0027) | | [MiniMax H3 Image LoRA](https://popnow.ai/docs/models/minimax-h3-image-lora) | `minimax/h3-image-lora/text-to-image`
`minimax/h3-image-lora/edit` | 6.3 credits per image ($0.063) | | [Qwen Image 3.0 Pro](https://popnow.ai/docs/models/qwen-image-3-0-pro) | `alibaba/qwen-image-3.0-pro/text-to-image`
`alibaba/qwen-image-3.0-pro/edit` | 6 credits per image ($0.06) | | [Qwen Image 3.0](https://popnow.ai/docs/models/qwen-image-3-0) | `alibaba/qwen-image-3.0/text-to-image`
`alibaba/qwen-image-3.0/edit` | 4.5 credits per image ($0.045) | | [Anima Turbo](https://popnow.ai/docs/models/anima-turbo) | `circlestone-labs/anima-turbo/text-to-image`
`circlestone-labs/anima-turbo/edit` | 1.8 credits per image ($0.018) | | [Anima Turbo LoRA](https://popnow.ai/docs/models/anima-turbo-lora) | `circlestone-labs/anima-turbo-lora/text-to-image`
`circlestone-labs/anima-turbo-lora/edit` | 2.7 credits per image ($0.027) | | [Krea 2 Turbo LoRA](https://popnow.ai/docs/models/krea-2-turbo-lora) | `krea/2-turbo-lora/text-to-image`
`krea/2-turbo-lora/edit` | 3.24 credits per image ($0.0324) | | [Krea 2 Large](https://popnow.ai/docs/models/krea-2-large) | `krea/2-large/text-to-image` | 9 credits per image ($0.09) | | [Krea 2 Medium](https://popnow.ai/docs/models/krea-2-medium) | `krea/2-medium/text-to-image` | 4.5 credits per image ($0.045) | | [Krea 2 Medium Turbo](https://popnow.ai/docs/models/krea-2-medium-turbo) | `krea/2-medium-turbo/text-to-image` | 2.25 credits per image ($0.0225) | | [GPT Image 2](https://popnow.ai/docs/models/gpt-image-2) | `openai/gpt-image-2/text-to-image`
`openai/gpt-image-2/edit` | 0.27 credits per image ($0.0027) | | [Wan 2.7 Pro](https://popnow.ai/docs/models/wan-2-7-pro) | `alibaba/wan-2.7-pro/text-to-image`
`alibaba/wan-2.7-pro/edit` | 11.25 credits per image ($0.1125) | | [Wan 2.7](https://popnow.ai/docs/models/wan-2-7) | `alibaba/wan-2.7/text-to-image`
`alibaba/wan-2.7/edit` | 4.5 credits per image ($0.045) | | [Nano Banana 2](https://popnow.ai/docs/models/nano-banana-2) | `google/nano-banana-2/text-to-image`
`google/nano-banana-2/edit` | 5.4 credits per image ($0.054) | | [Seedream 5.0 Lite](https://popnow.ai/docs/models/seedream-5-0-lite) | `bytedance/seedream-5.0-lite/text-to-image`
`bytedance/seedream-5.0-lite/edit` | 5.18 credits per image ($0.0518) | | [P-Image LoRA](https://popnow.ai/docs/models/p-image-lora) | `pruna-ai/p-image-lora/text-to-image`
`pruna-ai/p-image-lora/edit` | 0.9 credits per image ($0.009) | | [Qwen Image 2](https://popnow.ai/docs/models/alibaba-qwen-image-2) | `alibaba/qwen-image-2/text-to-image`
`alibaba/qwen-image-2/edit` | 5.25 credits per image ($0.0525) | | [Z-Image Base LoRA](https://popnow.ai/docs/models/z-image-base-lora) | `alibaba/z-image-base-lora/text-to-image`
`alibaba/z-image-base-lora/edit` | 2.16 credits per image ($0.0216) | | [FLUX.2 Klein 9B LoRA](https://popnow.ai/docs/models/flux-2-klein-9b-lora) | `black-forest-labs/flux-2-klein-9b-lora/text-to-image`
`black-forest-labs/flux-2-klein-9b-lora/edit` | 2.7 credits per image ($0.027) | | [FLUX.2 Klein Base 9B LoRA](https://popnow.ai/docs/models/flux-2-klein-base-9b-lora) | `black-forest-labs/flux-2-klein-base-9b-lora/text-to-image`
`black-forest-labs/flux-2-klein-base-9b-lora/edit` | 3.6 credits per image ($0.036) | | [FLUX.2 Klein 4B LoRA](https://popnow.ai/docs/models/flux-2-klein-4b-lora) | `black-forest-labs/flux-2-klein-4b-lora/text-to-image`
`black-forest-labs/flux-2-klein-4b-lora/edit` | 2.34 credits per image ($0.0234) | | [FLUX.2 Klein Base 4B LoRA](https://popnow.ai/docs/models/flux-2-klein-base-4b-lora) | `black-forest-labs/flux-2-klein-base-4b-lora/text-to-image`
`black-forest-labs/flux-2-klein-base-4b-lora/edit` | 2.7 credits per image ($0.027) | | [FLUX.2 Klein 9B](https://popnow.ai/docs/models/flux-2-klein-9b) | `black-forest-labs/flux-2-klein-9b/text-to-image`
`black-forest-labs/flux-2-klein-9b/edit` | 1.65 credits per image ($0.0165) | | [FLUX.2 Klein 4B](https://popnow.ai/docs/models/flux-2-klein-4b) | `black-forest-labs/flux-2-klein-4b/text-to-image`
`black-forest-labs/flux-2-klein-4b/edit` | 1.39 credits per image ($0.0139) | | [Qwen Image 2512 LoRA](https://popnow.ai/docs/models/qwen-image-2512-lora) | `alibaba/qwen-image-2512-lora/text-to-image`
`alibaba/qwen-image-2512-lora/edit` | 4.5 credits per image ($0.045) | | [Z-Image](https://popnow.ai/docs/models/z-image) | `alibaba/z-image-turbo/text-to-image` | 1.5 credits per image ($0.015) | | [Z-Image Turbo LoRA](https://popnow.ai/docs/models/z-image-turbo-lora) | `alibaba/z-image-turbo-lora/text-to-image`
`alibaba/z-image-turbo-lora/edit` | 1.8 credits per image ($0.018) | | [FLUX.2 Dev LoRA](https://popnow.ai/docs/models/flux-2-dev-lora) | `black-forest-labs/flux-2-dev-lora/text-to-image`
`black-forest-labs/flux-2-dev-lora/edit` | 3.24 credits per image ($0.0324) | | [FLUX.2 Pro](https://popnow.ai/docs/models/flux-2-pro) | `black-forest-labs/flux-2-pro/text-to-image`
`black-forest-labs/flux-2-pro/edit` | 4.5 credits per image ($0.045) | | [FLUX.2 Flex](https://popnow.ai/docs/models/flux-2-flex) | `black-forest-labs/flux-2-flex/text-to-image`
`black-forest-labs/flux-2-flex/edit` | 8.67 credits per image ($0.0867) | | [FLUX.2 Dev](https://popnow.ai/docs/models/flux-2-dev) | `black-forest-labs/flux-2-dev/text-to-image`
`black-forest-labs/flux-2-dev/edit` | 1.74 credits per image ($0.0174) | | [Jib Mix Qwen](https://popnow.ai/docs/models/jib-mix-qwen-image) | `j1b/jib-mix-qwen-image/text-to-image` | 3.6 credits per image ($0.036) | | [Jib Mix Qwen LoRA](https://popnow.ai/docs/models/jib-mix-qwen-image-lora) | `j1b/jib-mix-qwen-image-lora/text-to-image` | 4.5 credits per image ($0.045) | | [Qwen Image Edit 2509 LoRA](https://popnow.ai/docs/models/qwen-image-edit-2509-lora) | `alibaba/qwen-image-edit-2509-lora/edit` | 4.5 credits per image ($0.045) | | [Seedream 4.0](https://popnow.ai/docs/models/seedream-4-0) | `bytedance/seedream-4.0/text-to-image`
`bytedance/seedream-4.0/edit` | 4.5 credits per image ($0.045) | | [Prefect Pony XL LoRA](https://popnow.ai/docs/models/prefect-pony-xl-lora) | `prefect/pony-xl-lora/text-to-image` | 3.6 credits per image ($0.036) | | [Prefect Pony XL](https://popnow.ai/docs/models/prefect-pony-xl) | `prefect/pony-xl/text-to-image` | 2.25 credits per image ($0.0225) | | [Qwen Image 1.0 LoRA](https://popnow.ai/docs/models/qwen-image-1-0-lora) | `alibaba/qwen-image-1.0-lora/text-to-image`
`alibaba/qwen-image-1.0-lora/edit` | 4.5 credits per image ($0.045) | | [FLUX.1 Krea Dev LoRA](https://popnow.ai/docs/models/flux-1-krea-dev-lora) | `black-forest-labs/flux-1-krea-dev-lora/text-to-image`
`black-forest-labs/flux-1-krea-dev-lora/edit` | 3.6 credits per image ($0.036) | | [Wan 2.2 Image LoRA](https://popnow.ai/docs/models/wan-2-2-image-lora) | `alibaba/wan-2.2-image-lora/text-to-image` | 4.5 credits per image ($0.045) | | [Neta Lumina](https://popnow.ai/docs/models/neta-lumina) | `neta-art/neta-lumina/text-to-image` | 1.8 credits per image ($0.018) | | [FLUX.1 Dev ControlNet LoRA](https://popnow.ai/docs/models/flux-1-dev-controlnet-lora) | `black-forest-labs/flux-1-dev-controlnet-lora/text-to-image` | 5.4 credits per image ($0.054) | | [Wan 2.1 Image LoRA](https://popnow.ai/docs/models/wan-2-1-image-lora) | `alibaba/wan-2.1-image-lora/text-to-image`
`alibaba/wan-2.1-image-lora/edit` | 4.5 credits per image ($0.045) | | [FLUX.1 Fill Dev LoRA](https://popnow.ai/docs/models/flux-1-fill-dev-lora) | `black-forest-labs/flux-1-fill-dev-lora/edit` | 6.3 credits per image ($0.063) | | [FLUX.1 Dev LoRA](https://popnow.ai/docs/models/flux-1-dev-lora) | `black-forest-labs/flux-1-dev-lora/text-to-image`
`black-forest-labs/flux-1-dev-lora/edit` | 2.7 credits per image ($0.027) | | [FLUX.1 Schnell LoRA](https://popnow.ai/docs/models/flux-1-schnell-lora) | `black-forest-labs/flux-1-schnell-lora/text-to-image`
`black-forest-labs/flux-1-schnell-lora/edit` | 0.9 credits per image ($0.009) | | [Face Swap Pro](https://popnow.ai/docs/models/face-swap-pro-v1) | `popnow/face-swap-pro-v1/edit` | 24 credits per image ($0.24) | | [Face Swap Plus](https://popnow.ai/docs/models/face-swap-v1-plus) | `popnow/face-swap-v1-plus/edit` | 24 credits per image ($0.24) | | [Celebrity Look-Alike](https://popnow.ai/docs/models/look-alike-v1) | `popnow/look-alike-v1/edit` | 7.5 credits per image ($0.075) | | [Face Swap](https://popnow.ai/docs/models/face-swap-v1) | `popnow/face-swap-v1/edit` | 1.95 credits per image ($0.0195) | | [Head Swap](https://popnow.ai/docs/models/head-swap-v1) | `popnow/head-swap-v1/edit` | 1.95 credits per image ($0.0195) | | [Background Remover](https://popnow.ai/docs/models/background-remover-v1) | `popnow/background-remover-v1/edit` | 0.72 credits per image ($0.0072) | | [Image Upscaler](https://popnow.ai/docs/models/image-upscaler-v1) | `popnow/image-upscaler-v1/upscale` | 1.8 credits per image ($0.018) | | [Object Eraser](https://popnow.ai/docs/models/object-eraser-v1) | `popnow/object-eraser-v1/edit` | 4.5 credits per image ($0.045) | | [Image Expander](https://popnow.ai/docs/models/image-expander-v1) | `popnow/image-expander-v1/edit` | 3.6 credits per image ($0.036) | ### Video (44) | Model | Model IDs | From | | --- | --- | --- | | [MiniMax H3](https://popnow.ai/docs/models/minimax-h3) | `minimax/h3/text-to-video`
`minimax/h3/image-to-video`
`minimax/h3/reference-to-video`
`minimax/h3/video-extend` | 5.55 credits per second of output ($0.0555) | | [Seedance 2.5](https://popnow.ai/docs/models/seedance-2-5) | `bytedance/seedance-2.5/text-to-video`
`bytedance/seedance-2.5/image-to-video`
`bytedance/seedance-2.5/reference-to-video`
`bytedance/seedance-2.5/video-extend` | 11.07 credits per second of output ($0.1107) | | [Kling 3.0](https://popnow.ai/docs/models/kling-3-0) | `kling/3.0/text-to-video`
`kling/3.0/image-to-video`
`kling/3.0/character-animation` | 10.08 credits per second of output ($0.1008) | | [Vidu Q4 Preview](https://popnow.ai/docs/models/vidu-q4-preview) | `vidu/q4-preview/image-to-video`
`vidu/q4-preview/reference-to-video`
`vidu/q4-preview/video-extend` | 6.75 credits per second of output ($0.0675) | | [Wan 3.0](https://popnow.ai/docs/models/wan-3-0) | `alibaba/wan-3.0/text-to-video`
`alibaba/wan-3.0/image-to-video`
`alibaba/wan-3.0/reference-to-video`
`alibaba/wan-3.0/video-extend` | 6.75 credits per second of output ($0.0675) | | [Kling O3](https://popnow.ai/docs/models/kling-o3) | `kling/o3/text-to-video`
`kling/o3/image-to-video`
`kling/o3/reference-to-video`
`kling/o3/video-edit` | 10.08 credits per second of output ($0.1008) | | [MiniMax H3 Singularity LoRA](https://popnow.ai/docs/models/minimax-h3-singularity-lora) | `minimax/h3-singularity-lora/image-to-video`
`minimax/h3-singularity-lora/reference-to-video`
`minimax/h3-singularity-lora/video-extend` | 9 credits per second of output ($0.09) | | [MiniMax H3 Max](https://popnow.ai/docs/models/minimax-h3-max) | `minimax/h3-max/text-to-video`
`minimax/h3-max/image-to-video`
`minimax/h3-max/video-extend` | 7.5 credits per second of output ($0.075) | | [LTX 2.5](https://popnow.ai/docs/models/ltx-2-5) | `lightricks/ltx-2.5/text-to-video`
`lightricks/ltx-2.5/image-to-video` | 13.5 credits per second of output ($0.135) | | [Wan 3.0 Prime](https://popnow.ai/docs/models/wan-3-0-prime) | `alibaba/wan-3.0-prime/text-to-video`
`alibaba/wan-3.0-prime/image-to-video`
`alibaba/wan-3.0-prime/reference-to-video`
`alibaba/wan-3.0-prime/video-extend` | 9.18 credits per second of output ($0.0918) | | [Wan 3.0 Pro Prime](https://popnow.ai/docs/models/wan-3-0-pro-prime) | `alibaba/wan-3.0-pro-prime/text-to-video`
`alibaba/wan-3.0-pro-prime/image-to-video`
`alibaba/wan-3.0-pro-prime/reference-to-video`
`alibaba/wan-3.0-pro-prime/video-extend` | 35.1 credits per second of output ($0.351) | | [Wan 3.0 Pro](https://popnow.ai/docs/models/wan-3-0-pro) | `alibaba/wan-3.0-pro/text-to-video`
`alibaba/wan-3.0-pro/image-to-video`
`alibaba/wan-3.0-pro/reference-to-video`
`alibaba/wan-3.0-pro/video-extend` | 21.6 credits per second of output ($0.216) | | [FLUX 3](https://popnow.ai/docs/models/flux-3) | `black-forest-labs/flux-3/upscale`
`black-forest-labs/flux-3/text-to-video`
`black-forest-labs/flux-3/image-to-video`
`black-forest-labs/flux-3/video-extend` | 9 credits per second of output ($0.09) | | [MiniMax H3 LoRA](https://popnow.ai/docs/models/minimax-h3-lora) | `minimax/h3-lora/text-to-video`
`minimax/h3-lora/image-to-video`
`minimax/h3-lora/reference-to-video`
`minimax/h3-lora/video-extend` | 7.5 credits per second of output ($0.075) | | [HappyHorse 1.1](https://popnow.ai/docs/models/happyhorse-1-1) | `alibaba/happyhorse-1.1/text-to-video`
`alibaba/happyhorse-1.1/image-to-video`
`alibaba/happyhorse-1.1/reference-to-video` | 21 credits per second of output ($0.21) | | [Kling 3.0 Turbo](https://popnow.ai/docs/models/kling-3-0-turbo) | `kling/3.0-turbo/text-to-video`
`kling/3.0-turbo/image-to-video` | 16.8 credits per second of output ($0.168) | | [Seedance 2.0 Mini](https://popnow.ai/docs/models/seedance-2-0-mini) | `bytedance/seedance-2.0-mini/text-to-video`
`bytedance/seedance-2.0-mini/image-to-video`
`bytedance/seedance-2.0-mini/reference-to-video`
`bytedance/seedance-2.0-mini/video-extend` | 1.65 credits per second of output ($0.0165) | | [LTX-2.3 LoRA](https://popnow.ai/docs/models/ltx-2-3-lora) | `lightricks/ltx-2.3-lora/text-to-video`
`lightricks/ltx-2.3-lora/image-to-video` | 3.6 credits per second of output ($0.036) | | [Seedance 2.0](https://popnow.ai/docs/models/seedance-2-0) | `bytedance/seedance-2.0/text-to-video`
`bytedance/seedance-2.0/image-to-video`
`bytedance/seedance-2.0/reference-to-video`
`bytedance/seedance-2.0/video-extend` | 6.47 credits per second of output ($0.0647) | | [Seedance 2.0 Fast](https://popnow.ai/docs/models/seedance-2-0-fast) | `bytedance/seedance-2.0-fast/text-to-video`
`bytedance/seedance-2.0-fast/image-to-video`
`bytedance/seedance-2.0-fast/reference-to-video`
`bytedance/seedance-2.0-fast/video-extend` | 3.38 credits per second of output ($0.0338) | | [Vidu Q3 Turbo](https://popnow.ai/docs/models/vidu-q3-turbo) | `vidu/q3-turbo/image-to-video`
`vidu/q3-turbo/video-extend` | 6.3 credits per second of output ($0.063) | | [Vidu Q3](https://popnow.ai/docs/models/vidu-q3) | `vidu/q3/image-to-video` | 10.5 credits per second of output ($0.105) | | [Vidu Q3 Pro](https://popnow.ai/docs/models/vidu-q3-pro) | `vidu/q3-pro/image-to-video` | 8.1 credits per second of output ($0.081) | | [LTX-2 19B](https://popnow.ai/docs/models/ltx-2-19b) | `lightricks/ltx-2-19b/text-to-video`
`lightricks/ltx-2-19b/image-to-video`
`lightricks/ltx-2-19b/video-extend` | 2.16 credits per second of output ($0.0216) | | [LTX-2 19B LoRA](https://popnow.ai/docs/models/ltx-2-19b-lora) | `lightricks/ltx-2-19b-lora/text-to-video`
`lightricks/ltx-2-19b-lora/image-to-video` | 2.7 credits per second of output ($0.027) | | [Seedance 1.5 Pro](https://popnow.ai/docs/models/seedance-1-5-pro) | `bytedance/seedance-1.5-pro/text-to-video`
`bytedance/seedance-1.5-pro/image-to-video`
`bytedance/seedance-1.5-pro/video-extend` | 1.68 credits per second of output ($0.0168) | | [Wan 2.6 Flash](https://popnow.ai/docs/models/wan-2-6-flash) | `alibaba/wan-2.6-flash/image-to-video`
`alibaba/wan-2.6-flash/reference-to-video` | 3.38 credits per second of output ($0.0338) | | [Wan 2.6](https://popnow.ai/docs/models/wan-2-6) | `alibaba/wan-2.6/text-to-video`
`alibaba/wan-2.6/image-to-video`
`alibaba/wan-2.6/reference-to-video` | 9.75 credits per second of output ($0.0975) | | [Kling Avatar 2.0](https://popnow.ai/docs/models/kling-avatar-2-0) | `kling/avatar-2.0/talking-avatar` | 7.68 credits per second of output ($0.0768) | | [Kling 2.6](https://popnow.ai/docs/models/kling-2-6) | `kling/2.6/text-to-video`
`kling/2.6/image-to-video`
`kling/2.6/character-animation` | 10.35 credits per second of output ($0.1035) | | [HunyuanVideo 1.5](https://popnow.ai/docs/models/hunyuan-video-1-5) | `tencent/hunyuan-video-1.5/text-to-video`
`tencent/hunyuan-video-1.5/image-to-video` | 3.6 credits per second of output ($0.036) | | [Wan 2.5](https://popnow.ai/docs/models/wan-2-5) | `alibaba/wan-2.5/text-to-video`
`alibaba/wan-2.5/image-to-video`
`alibaba/wan-2.5/video-extend` | 6.75 credits per second of output ($0.0675) | | [Wan 2.2 Animate](https://popnow.ai/docs/models/wan-2-2-animate) | `alibaba/wan-2.2-animate/character-animation` | 7.95 credits per second of output ($0.0795) | | [Kling 2.5 Turbo](https://popnow.ai/docs/models/kling-2-5-turbo) | `kling/2.5-turbo/text-to-video`
`kling/2.5-turbo/image-to-video` | 8.4 credits per second of output ($0.084) | | [Wan 2.2 LoRA](https://popnow.ai/docs/models/wan-2-2-lora) | `alibaba/wan-2.2-lora/text-to-video`
`alibaba/wan-2.2-lora/image-to-video`
`alibaba/wan-2.2-lora/video-extend` | 3.6 credits per second of output ($0.036) | | [Wan 2.2](https://popnow.ai/docs/models/wan-2-2) | `alibaba/wan-2.2/text-to-video`
`alibaba/wan-2.2/image-to-video` | 36 credits per request ($0.36) | | [Wan 2.2 5B LoRA](https://popnow.ai/docs/models/wan-2-2-5b-lora) | `alibaba/wan-2.2-5b-lora/text-to-video`
`alibaba/wan-2.2-5b-lora/image-to-video` | 18 credits per request ($0.18) | | [Kling 2.1 Master](https://popnow.ai/docs/models/kling-2-1-master) | `kling/2.1-master/text-to-video`
`kling/2.1-master/image-to-video` | 32.4 credits per second of output ($0.324) | | [Kling 2.1](https://popnow.ai/docs/models/kling-2-1) | `kling/2.1/image-to-video` | 5.25 credits per second of output ($0.0525) | | [Wan 2.1 LoRA](https://popnow.ai/docs/models/wan-2-1-lora) | `alibaba/wan-2.1-lora/text-to-video`
`alibaba/wan-2.1-lora/image-to-video`
`alibaba/wan-2.1-lora/video-edit` | 36 credits per request ($0.36) | | [Video Face Swap](https://popnow.ai/docs/models/face-swap-video-v1) | `popnow/face-swap-video-v1/video-edit` | 3.9 credits per second of output ($0.039) | | [Video Upscaler](https://popnow.ai/docs/models/video-upscaler-v1) | `popnow/video-upscaler-v1/upscale` | 0.9 credits per second of output ($0.009) | | [Talking Avatar](https://popnow.ai/docs/models/talking-avatar-v1) | `popnow/talking-avatar-v1/talking-avatar` | 2.7 credits per second of output ($0.027) | | [Lip Sync](https://popnow.ai/docs/models/lip-sync-v1) | `popnow/lip-sync-v1/lip-sync` | 1.8 credits per second of output ($0.018) | ### Audio (17) | Model | Model IDs | From | | --- | --- | --- | | [Suno v6](https://popnow.ai/docs/models/suno-v6) | `suno/v6/text-to-audio` | 10.8 credits per request ($0.108) | | [Gemini 3.8 Flash TTS](https://popnow.ai/docs/models/gemini-3-8-flash-tts) | `google/gemini-3.8-flash-tts/text-to-speech` | 0.05 credits per second of output ($0.0005) | | [MiniMax Speech 2.8 HD](https://popnow.ai/docs/models/minimax-speech-2-8-hd) | `minimax/speech-2.8-hd/text-to-speech` | 18 credits per 1,000 characters ($0.18) | | [MiniMax Music 3.0](https://popnow.ai/docs/models/minimax-music-3-0) | `minimax/music-3.0/text-to-audio` | 27 credits per request ($0.27) | | [Seed Audio 1.0](https://popnow.ai/docs/models/seed-audio-1-0) | `bytedance/seed-audio-1.0/text-to-audio` | 54 credits per request ($0.54) | | [Gemini 3.8 Flash-Lite TTS](https://popnow.ai/docs/models/gemini-3-8-flash-lite-tts) | `google/gemini-3.8-flash-lite-tts/text-to-speech` | 0.03 credits per second of output ($0.0003) | | [Suno v6 Mini](https://popnow.ai/docs/models/suno-v6-mini) | `suno/v6-mini/text-to-audio` | 10.8 credits per request ($0.108) | | [Suno v6 Wild](https://popnow.ai/docs/models/suno-v6-wild) | `suno/v6-wild/text-to-audio` | 10.8 credits per request ($0.108) | | [HeartMuLa Transcribe](https://popnow.ai/docs/models/heartmula-transcribe) | `heartmula/transcribe/speech-to-text` | 7.5 credits per request ($0.075) | | [Grok STT](https://popnow.ai/docs/models/grok-stt) | `xai/grok-stt/speech-to-text` | 0.01 credits per second of output ($0.0001) | | [Grok TTS](https://popnow.ai/docs/models/grok-tts) | `xai/grok-tts/text-to-speech` | 2.7 credits per 1,000 characters ($0.027) | | [Seed Speech 2.0](https://popnow.ai/docs/models/seed-speech-2-0) | `bytedance/seed-speech-2.0/text-to-speech` | 10.8 credits per 1,000 characters ($0.108) | | [MiniMax Speech 2.8 Turbo](https://popnow.ai/docs/models/minimax-speech-2-8-turbo) | `minimax/speech-2.8-turbo/text-to-speech` | 10.8 credits per 1,000 characters ($0.108) | | [Qwen3 TTS](https://popnow.ai/docs/models/qwen-3-tts) | `alibaba/qwen-3-tts/text-to-speech` | 10.2 credits per 1,000 characters ($0.102) | | [Chatterbox](https://popnow.ai/docs/models/chatterbox-v1) | `resemble-ai/chatterbox-v1/text-to-speech` | 5.4 credits per request ($0.054) | | [Gemini 2.5 Pro TTS](https://popnow.ai/docs/models/gemini-2-5-pro-tts) | `google/gemini-2.5-pro-tts/text-to-speech` | 0.07 credits per second of output ($0.0007) | | [Video Sound Effects](https://popnow.ai/docs/models/foley-v1) | `popnow/foley-v1/video-to-audio` | 0.18 credits per second of output ($0.0018) | --- # Tasks Canonical: https://popnow.ai/docs/tasks > Create a task, follow its state, read the result, and use wait, the task list, retry and purge. Every generation is a **task**. You create it with one call, it runs on its own, and you collect the result when it is ready — by asking for it, by waiting on the first call, or by receiving a [webhook](https://popnow.ai/docs/webhooks). 1. `POST /jobs/createTask` → `taskId` 2. `GET /jobs/recordInfo` → `state` 3. `GET output.assets[].url` Three calls, in this order. Poll recordInfo until the state is final, then download the file before its link expires. ## Create a task [#create-a-task] ```bash 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": "alibaba/wan-3.0/image-to-video", "input": { "image_url": "https://media.popnow.ai/movie/lighthouse.webp", "prompt": "Waves roll in slowly, the camera drifts toward the lighthouse", "resolution": "480p", "duration_seconds": 5 }, "callBackUrl": "https://your-app.example.com/hooks/popnow" }' ``` ### Body [#create-body] | Field | Required | Meaning | | ---------------------------- | -------- | -------------------------------------------------------------------------------- | | `model` | Yes | The model ID from [Models](https://popnow.ai/docs/models) | | `input` | Yes | The model's own fields, validated against its input schema | | `callBackUrl` | No | Public HTTPS URL that receives the finished task. See [Webhooks](https://popnow.ai/docs/webhooks) | | `quoteId`, `expectedCredits` | No | Lock in a price you confirmed with [jobs/quote](https://popnow.ai/docs/billing#quote) | ### Headers [#create-headers] | Header | Meaning | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `Authorization: Bearer sk-popnow-…` | Your API key | | `Idempotency-Key` | Strongly recommended. A retry with the same key returns the original task. See [Idempotency](https://popnow.ai/docs/idempotency) | | `X-PopNow-Retention` | Optional. Seconds to keep this task's outputs and prompt. It can only shorten how long things are kept, never extend it | | `Accept-Language` | Optional. Language of error messages. See [Errors](https://popnow.ai/docs/errors#error-language) | ### Response [#create-response] A new task comes back as HTTP `202`: ```json { "code": 200, "msg": "success", "data": { "taskId": "job_01JH8Z4Q5W2N7R3K9M6P1C0VXD", "state": "queued", "estimatedCredits": 34, "estimatedCost": "0.34", "deadlineAt": "2026-10-16T09:40:00Z" }, "request_id": "req_…" } ``` `202` means **accepted**, not finished. `estimatedCredits` is the amount held from your balance — the most this task can cost (`estimatedCost` is the same amount in US dollars). If creation fails instead, nothing is created and nothing is held; the `code` tells you why ([Errors](https://popnow.ai/docs/errors)). ## States [#states] `queued` → `running` → `succeeded` / `failed` / `expired` A task is in exactly one state at a time. Only succeeded is charged; every other final state returns the held credits in full. | State | Final? | Meaning | | ----------- | ------ | -------------------------------------------------------------------------- | | `queued` | No | Accepted, waiting to start | | `running` | No | Generating | | `succeeded` | Yes | Done; the files are in `output.assets` | | `failed` | Yes | Could not finish; see `errorCode` and `errorMessage`. The hold is released | | `expired` | Yes | Passed its deadline without finishing. The hold is released | | `canceled` | Yes | Only on old records. An accepted task cannot be canceled today | Treat any state you do not recognise as "not final yet" and keep reading. ## Skip the polling loop with `wait` [#wait] Add `?wait=` (up to 60) to `createTask` and the connection stays open until the task finishes or the time runs out: * Finished in time → HTTP `200` with the full task record, the same shape `recordInfo` returns. Check `data.state`: final does not always mean succeeded. * Not finished → the ordinary `202`, and you carry on polling or wait for the webhook. Use it for images and short audio; video almost always outlasts 60 seconds. Disconnecting early does not stop or cancel the task. ## Read a task [#record-info] ```bash curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=job_01JH8Z4Q5W2N7R3K9M6P1C0VXD" \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` | Field | Meaning | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `taskId`, `model`, `state` | Which task, which model, where it is | | `input` | The input as it was run, including the `seed` that was picked if you left it out | | `output.assets[]` | The files. Each has `url`, `expiresAt`, `mime`, and when it applies `width`, `height`, `durationSeconds`, `bytes`, `role` | | `errorCode`, `errorMessage` | Why a task failed. Branch on `errorCode`, show `errorMessage` | | `credits`, `cost`, `settled` | While `settled` is `false`, `credits` is the hold; once `true`, it is the final charge. `cost` is the same amount in US dollars | | `createdAt`, `completedAt`, `deadlineAt` | Timestamps; `deadlineAt` is when an unfinished task expires | | `retention.outputsExpireAt` | When this task's files will be removed | | `contentState` | `present`, or `expired` / `purged` once the content has been removed | ### Three different expiry times [#expiry-times] * **`deadlineAt`** — how long the task may run. Past it, the task becomes `expired` and the hold is released. * **`output.assets[].expiresAt`** — how long that one signed link works, about 20 minutes. Read the task again for a fresh link; the file itself is still there. * **`retention.outputsExpireAt`** — when the stored file is removed. Download anything you want to keep before then, or save it to your [asset library](https://popnow.ai/docs/assets). ### What `output` contains [#output] Most tasks return one asset. Some return more: several images when the model makes variations, a joined clip plus the new segment for video extension (`role: "video"` then `role: "segment"`), or a base image plus transparent layers for layer separation (each with a `layer` object). Speech-to-text returns the transcript in `output.text`. Asset links are bearer links: anyone with the URL can download the file until it expires, and you should **not** send your API key when fetching them. ## Webhooks first, polling as a fallback [#callbacks-or-polling] Polling works, but a webhook is cheaper for both sides: you hear about the result the moment it exists and spend nothing on requests in between. A robust integration uses both — webhooks for the normal path, and an occasional `recordInfo` sweep for any task whose webhook you did not receive. When you poll, back off: every 2–3 seconds for images, every 10–15 seconds for video, and stop at `deadlineAt`. ## The task list [#list] `GET /api/v1/jobs` returns the tasks this key created, newest first, without inputs or outputs: ```bash curl "https://api.popnow.ai/api/v1/jobs?state=failed&from=2026-10-01&limit=50" \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` Filters: `from` and `to` (UTC dates, at most 92 days apart; the default is the last seven days), `state`, `model`, `limit` (up to 100). When `hasMore` is `true`, pass `nextCursor` back as `cursor` with the same filters. ## Retry a failed task [#retry] `POST /api/v1/jobs/retry` with `{ "taskId": "…" }` creates a **new** task from a `failed` or `expired` one, using its original input at today's price. The original task is never changed. It answers `409` while the source task is not final yet, and cannot retry a task that succeeded — create a new one instead. ## Delete a task's content [#purge] `POST /api/v1/jobs/purge` with `{ "taskId": "…" }` deletes the task's files, its result and its stored prompt. The billing record stays: the charge, the model, the state and the timestamps remain, and the response says so with `billingRetained: true`. Only finished tasks can be purged, and repeating the call is safe. ## Next [#next] * [Webhooks](https://popnow.ai/docs/webhooks) — receive results instead of polling. * [Media](https://popnow.ai/docs/media) — upload your own reference files. * [Errors](https://popnow.ai/docs/errors) — every code and whether to retry. --- # Media Canonical: https://popnow.ai/docs/media > Send reference images, video and audio by URL, inline or by upload, and download the files a task produces. Many models take files as input — a photo to animate, a clip to extend, a voice sample, a song. There are four ways to hand one over, and one way to get results back. ## Four ways to send a file [#inputs] | You have | Send it as | Best for | | ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- | | A public HTTPS link | The URL itself, e.g. `"image_url": "https://…/photo.jpg"` | Files you already host | | A small image in memory | A data URI, e.g. `"image_url": "data:image/png;base64,…"` | Quick tests, images under 1 MiB | | A local file | An upload, then `"image_url": "popnow://f/fil_…"` | Anything else, including video and audio | | A file in your asset library | `"image_url": "popnow://a/ast_…"` | Files you reuse across many tasks. See [Assets](https://popnow.ai/docs/assets) | Whichever you choose, the file goes in the field the model's schema declares for it (`image_url`, `image_urls`, `video_url`, `audio_url` …). ### Public links [#https-media] The link must return **the file itself** and stay reachable without login while the task runs. A share page from a cloud drive or social network returns a web page, not the file, and the task fails with `invalid_asset`; download the file and upload it instead. ### Inline images [#inline] Images can be sent as a standard Base64 data URI with its MIME type: JPEG, PNG, WebP or GIF, at most 1 MiB decoded and 8,192 pixels per side each, up to 16 images per request, and the whole JSON body under 2 MiB. Bare Base64, SVG and inline video or audio are not accepted. Inline images are stored as uploads on your account, so the task's `input` shows a `popnow://` URI instead of echoing the bytes back. ## Upload a file [#upload] Uploading takes three calls: ask for an upload ticket, `PUT` the bytes, then commit. ### Ask for a ticket [#request-a-ticket] ```bash FILE=reference.png CONTENT_TYPE=image/png BYTES=$(wc -c < "$FILE" | tr -d ' ') TICKET=$(curl -s -X POST "https://api.popnow.ai/api/v1/common/upload-url" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"contentType\": \"$CONTENT_TYPE\", \"bytes\": $BYTES}") UPLOAD_URL=$(echo "$TICKET" | jq -r .data.uploadUrl) FILE_ID=$(echo "$TICKET" | jq -r .data.fileId) ``` The response carries `fileId`, `uploadUrl`, the exact `headers` to send, `expiresAt` (about 20 minutes away) and `maxBytes`. There is no file name: storage paths are always chosen by the server. ### PUT the bytes [#put-the-file] ```bash curl -s -o /dev/null -w "%{http_code}\n" -X PUT "$UPLOAD_URL" \ -H "Content-Type: $CONTENT_TYPE" \ -H "Content-Length: $BYTES" \ --data-binary @"$FILE" ``` Send every header from the ticket exactly as given — both are part of the signature — and **no** `Authorization` header. In a browser, leave out `Content-Length` and send a `File` or `Blob` of exactly the declared size; the browser sets it for you. ### Commit [#commit] ```bash curl -s -X POST "https://api.popnow.ai/api/v1/files/$FILE_ID/commit" \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` Commit checks the size, the type and the file signature, and returns the URI to use: ```json { "code": 200, "msg": "success", "data": { "fileId": "fil_01k3m8x9q2z4v7n5p6r8s0t1w3", "status": "ready", "uri": "popnow://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3", "contentType": "image/png", "bytes": 402118, "width": 1024, "height": 1024, "expiresAt": "2026-10-17T09:12:00Z" }, "request_id": "req_…" } ``` Put `uri` in the model's field, for example `"image_url": "popnow://f/fil_…"`. A `popnow://` URI only works with keys on your own account. Committing twice is safe. ### Upload limits [#upload-limits] | Type | `contentType` | Largest file | Longest duration | | ------------ | ---------------------------------------------------- | ------------ | ---------------- | | Image | `image/jpeg`, `image/png`, `image/webp`, `image/gif` | 10 MiB | — | | Video | `video/mp4`, `video/webm` | 90 MiB | 600 seconds | | Audio | `audio/mpeg`, `audio/wav` | 90 MiB | 600 seconds | | LoRA weights | `application/octet-stream` (a `.safetensors` file) | 2 GiB | — | A model can accept less than this; its schema says so per field. Uploaded files are temporary — the commit response's `expiresAt` tells you when the file is removed — so upload shortly before you create the task. To keep a file for repeated use, save it to your [asset library](https://popnow.ai/docs/assets). If a commit answers `40003`, the bytes you sent do not match the ticket (different size, type or content). Ask for a new ticket and upload again. ## Download results [#download] A finished task lists its files in `output.assets`, each with a ready-to-use `url`: ```bash curl -L -o result.mp4 "$(curl -s "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \ -H "Authorization: Bearer $POPNOW_API_KEY" | jq -r '.data.output.assets[0].url')" ``` * The link is signed and works for about **20 minutes** (`expiresAt`). Do not store it; store the `taskId` and read the task again for a fresh link. * Do not attach your API key when downloading. The link itself is the permission, and anyone holding it can download the file until it expires. * To mint a link without reading the whole task, call `POST /api/v1/common/download-url` with `{ "taskId": "…" }` (and optionally the asset's `key`). The files themselves are kept until the time in the task's `retention.outputsExpireAt`, or until you [purge](https://popnow.ai/docs/tasks#purge) the task. Copy anything you need beyond that to your own storage or your asset library. --- # Webhooks Canonical: https://popnow.ai/docs/webhooks > Receive finished tasks at your own URL, verify the signature, and handle retries and duplicates. A webhook lets PopNow tell you when a task is done, so you do not have to keep asking. There are two kinds: | | Task callback | Account endpoint | | --------- | ---------------------------------- | ----------------------------------------------------------- | | Set up | `callBackUrl` on each `createTask` | Once, in the API console | | Sent for | That one task | Every task on the account that matches the events you chose | | Signature | `X-Webhook-Signature` (below) | Standard Webhooks headers ([see below](#account-endpoints)) | Most integrations only need the task callback. ## 1. Get your signing secret [#signing-secret] Every delivery is signed with your account's webhook signing secret. Open the **Webhooks** page of the [API console](https://popnow.ai/api/console/webhooks), generate (or rotate) the secret and store it on your server as `POPNOW_WEBHOOK_SECRET`. It is shown only once. Rotating takes effect immediately, including for deliveries still being retried, so update your receivers right after. ## 2. Pass `callBackUrl` [#callback-url] ```json { "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" } ``` The URL must be public **HTTPS** on port 443 (or no port), without a username or password, and resolve only to public addresses — checked when you create the task and again when we connect. `http://localhost` cannot work; use a tunnel such as Cloudflare Tunnel or ngrok while developing, or poll. An invalid URL returns `400` with `msg: "Invalid callback URL"`. ## 3. What arrives [#the-request] When the task reaches a final state, PopNow sends one `POST`: ```http 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 ``` The body is exactly what `recordInfo` would return for the task: ```json { "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` is `200` even for a failed task: it describes the delivery, not the task. Read `data.state`. * `request_id` is the **delivery ID**. It stays the same across retries, so use it to ignore duplicates. * The signed links in `output` expire after about 20 minutes. Store the `taskId`, not the link. ## 4. Verify the signature [#signature] ```text signature = base64( HMAC-SHA256( secret, ".." ) ) ``` `taskId` is `data.taskId` from the body, `timestamp` is the `X-Webhook-Timestamp` header, and the last part is the SHA-256 of the **raw** request body in lowercase hex. Hash the bytes you received, not a re-serialised object: key order and whitespace would change the result. ```javascript title="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 title="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` is `2` for every new integration. Version `1` is an older format kept for existing receivers; if you ever see it, the task ID is the top-level `task_id`. ## Respond quickly [#respond] Any **2xx** within 15 seconds counts as received. Anything else — another status, a timeout, a refused connection — is retried. Acknowledge first and do the slow work (downloading, transcoding, notifying users) in a background job. ## Retries and duplicates [#retries] A failed delivery is retried 7 more times, after 10 s, 30 s, 2 min, 10 min, 30 min, 2 h and 6 h. Each retry is signed again with a fresh timestamp, so always check the timestamp against the current time rather than the first attempt. After the last retry the delivery is marked failed in the API console, where you can send it again by hand. The same task can therefore arrive more than once. Make your handler idempotent: store `request_id` with a unique index, and when you see one again, answer `200` and do nothing. If your server was down for longer than the retry window, list recent tasks with [`GET /api/v1/jobs`](https://popnow.ai/docs/tasks#list) and read any you missed with `recordInfo`. ## Account endpoints [#account-endpoints] An account endpoint is a URL registered once in the API console together with the events it wants. It receives events for every task on the account, so you never pass `callBackUrl` again. | Event | Sent when | | ---------------- | --------------------------------------------------------------------------- | | `task.completed` | Any of the four below. Subscribe to this unless you need to tell them apart | | `task.succeeded` | A task finished successfully | | `task.failed` | A task failed; the hold was released | | `task.expired` | A task passed its deadline; the hold was released | | `task.canceled` | Kept for old records only | Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com/) format, so off-the-shelf libraries work: ```text webhook-id: msg_01k3m8x9q2z4v7n5p6r8s0t1w3 webhook-timestamp: 1787045531 webhook-signature: v1,base64( HMAC-SHA256( key, ".." ) ) ``` Two differences from the task callback: the **body itself** is signed (no SHA-256 step), and the HMAC key is the Base64-decoded part of the secret after `whsec_` — not the secret string. During a secret rotation the header can hold two space-separated signatures; accept the request if either matches. `webhook-id` stays the same across retries: use it to ignore duplicates. The event's `data.object` is the task record at the moment the event was created. --- # Idempotency Canonical: https://popnow.ai/docs/idempotency > Retry after a timeout without generating twice or paying twice. An **idempotency key** is a label you attach to one piece of work, such as "render the video for order 8814". If the same labelled request reaches PopNow twice, the work is done once. ```http POST /api/v1/jobs/createTask Idempotency-Key: order-8814-render-1 ``` ## Why you want it [#why] A timeout leaves you guessing: maybe the request never arrived, maybe it arrived and only the answer was lost. Without a key, you either give up (and the user waits for nothing) or retry (and maybe generate and pay twice). With a key, retrying is always safe: you get either a new task or the one created a moment ago. ## How it behaves [#behaviour] * A repeat with the same key, from the same API key, returns **the original task** in its current state — no second task, no second hold, no second charge. * The key is tied to what you sent: `model`, the whole `input` and `callBackUrl`. Field order and whitespace do not matter, but any real change does. A changed request with an old key returns `409`; give it a new key. * A key is remembered for **24 hours** from first use. After that the same key creates a new, billed task, so stop automatic retries well before then. * Keys are up to 128 characters and only need to be unique within your account. Another API key on your account cannot reuse one: it gets `409` without the original task ID. | Endpoint | A repeat with the same key… | | ------------------------------ | -------------------------------------------------------------------------- | | `POST /api/v1/jobs/createTask` | returns the original task | | `POST /api/v1/jobs/retry` | returns the task that retry created (the key is scoped to the source task) | | `POST /v1/videos` | returns the original task | | `POST /api/v1/assets` | returns the asset already saved (the header is required here) | Read endpoints and upload/download link endpoints ignore the header. `jobs/purge` needs none: repeating it is already harmless. ## Choosing keys [#choosing-keys] * **One key per piece of work**, not per HTTP attempt. Every retry must reuse it. * Derive it from something you already store: `order-8814-render-1` can be recomputed after a crash; a random UUID cannot unless you save it first. * If you use a UUID, generate it **before** the first attempt and keep it next to the job it belongs to. > **Never generate the key inside the retry loop** > > ```javascript > // Wrong: a new key per attempt defeats the whole mechanism > for (let i = 0; i < 3; i++) await createTask(body, crypto.randomUUID()); > > // Right: one key, created once > const key = crypto.randomUUID(); > for (let i = 0; i < 3; i++) await createTask(body, key); > ``` ## Conflicts [#conflicts] All of these return `code: 409`: | What happened | Retry? | | --------------------------------------------------------- | ---------------------------------------------------------------------- | | Two requests with the same key arrived at the same moment | Yes — wait a few hundred milliseconds and resend with the **same** key | | The key was already used for a different request | No — the request changed, so it needs a new key | | Another API key on your account already used this key | No — retry with the API key that made the first attempt | Your code should not branch on `msg`, so allow one or two `409` retries and then surface the error to a person. ## Full example [#example] ```javascript title="JavaScript" const RETRYABLE = new Set([429, 500, 503, 50301]); // business codes from the body async function createTaskOnce(body, idempotencyKey) { for (let attempt = 0; attempt < 4; attempt++) { try { const res = await fetch('https://api.popnow.ai/api/v1/jobs/createTask', { method: 'POST', headers: { Authorization: `Bearer ${process.env.POPNOW_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(body), }); const json = await res.json(); if (json.code === 200) return json.data; // a new task, or the original one if (!RETRYABLE.has(json.code)) throw new Error(`${json.code} ${json.msg} (${json.request_id})`); } catch (err) { if (!(err instanceof TypeError)) throw err; // network error: safe to retry with the same key } await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 300)); } throw new Error('createTask did not succeed after 4 attempts'); } const task = await createTaskOnce( { model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A paper boat on a calm lake' } }, 'order-8814-render-1', ); ``` --- # Assets and LoRA files Canonical: https://popnow.ai/docs/assets > Keep files in your asset library and reuse them by URI, read your characters, and bring your own LoRA weights. Uploads and task results are temporary. When you want to reuse a file across many tasks — a product photo, a character's face, a voice sample — save it to your **asset library**. When a model accepts custom style weights, bring your own **LoRA** file. ## The asset library [#asset-library] A saved asset gets a stable URI, `popnow://a/ast_…`, that you can put in any matching media field of any model, for as long as you keep the asset: ```json { "model": "google/nano-banana-2.1/edit", "input": { "prompt": "Put this mug on a wooden café table, morning light", "image_urls": ["popnow://a/ast_01k3m8x9q2z4v7n5p6r8s0t1w3"] } } ``` The URI is a reference, not a download link: it only works in generation requests made with keys on your account. Assets stay until you delete them. ### Save a file [#save] `POST /api/v1/assets` copies a file into the library. Choose one source; the `Idempotency-Key` header is required, and saving the same source again returns the existing asset. | `source` | Also send | Saves | | ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | | `task_output` | `taskId` and the output's `key` or `index` | A file one of your succeeded tasks produced | | `upload` | `fileId` | A committed [upload](https://popnow.ai/docs/media#upload) | | `url` | `url` | A public `http`/`https` file (JPEG, PNG, WebP, GIF, MP4, WebM, MP3, WAV, FLAC, Ogg) | ```bash curl -X POST "https://api.popnow.ai/api/v1/assets" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: save-$TASK_ID-0" \ -d "{\"source\": \"task_output\", \"taskId\": \"$TASK_ID\", \"index\": 0, \"name\": \"Hero shot\"}" ``` The response is the asset: `id`, `uri`, `name`, `kind` (`image`, `video` or `audio`), size and dimensions, the `model` that generated it, and a private `previewUrl` valid for about 20 minutes. Store the `uri`, never the preview link. ### List, rename and delete [#manage] | Call | Does | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /api/v1/assets` | One page of assets. Filter with `kind`, `source`, `favorite`, `q` (name search), `collectionId`, `entityId`; sort with `sort=recent\|oldest\|name\|size`; continue with `cursor` | | `GET /api/v1/assets/{id}` | One asset with a fresh preview link | | `PATCH /api/v1/assets/{id}` | Change `name` and/or `favorite` | | `DELETE /api/v1/assets/{id}` | Delete it; its URI stops working in new requests | | `GET /api/v1/assets/entities` | Your characters: named sets of images arranged in the website, each image with its own URI | | `GET /api/v1/assets/usage` | Your storage plan, what you use, its limits and what the next plan unlocks | ### Storage plans [#plans] Storage opens with your first credit purchase and grows with what you have bought over time; it never shrinks, and saved assets always stay usable. `GET /api/v1/assets/usage` shows the numbers for your account. ### Errors [#errors] Asset library errors add a machine-readable `data.reason` to the usual envelope: | `data.reason` | HTTP | Meaning | | ---------------------- | ---- | ---------------------------------------------------------------------------- | | `asset_library_locked` | 400 | The library opens after your first credit purchase | | `asset_library_full` | 400 | Your plan's space or item count is used up — delete assets or move up a plan | | `asset_file_too_large` | 400 | The file is above your plan's per-file limit | | `asset_daily_limit` | 429 | Today's number of new assets is reached; wait for `Retry-After` (00:00 UTC) | | `asset_storage_full` | 503 | Storage is temporarily full on our side; try again later | Treat a reason you do not recognise like an absent one. Reading the library is limited to 60 requests per minute and saving to 30 per minute. ## Bring your own LoRA [#lora] Models whose ID ends in `-lora` accept a `loras` array — up to three weights files, each with a `path` and a `scale` (0 to 4): ```json { "model": "alibaba/qwen-image-2.1-lora/text-to-image", "input": { "prompt": "A cozy reading nook in watercolor style", "loras": [{ "path": "popnow://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3", "scale": 1 }] } } ``` `path` can be a public direct link to a `.safetensors` file, or a file you uploaded or imported: * **Upload** it like any other file with `contentType: application/octet-stream` (a `.safetensors` file up to 2 GiB; the ticket is valid for two hours). Commit checks that it is a complete safetensors file. * **Import it from a link** when the file needs a sign-in, for example a private Hugging Face repository: ```bash curl -X POST "https://api.popnow.ai/api/v1/files/import" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "your-name/your-lora-repo", "token": "hf_..."}' ``` `url` accepts a Hugging Face repo ID (`owner/repo`, when it holds exactly one `.safetensors` file), a Hugging Face repo or file page, or any direct HTTPS link. `token` is optional — a Hugging Face access token or a Civitai API key — and is used only for this one download, never stored. The response matches a [commit](https://popnow.ai/docs/media#commit): put its `uri` in `loras[].path`. A LoRA must match the base model it was trained for; weights for a different base usually fail or have no effect. You are responsible for having the right to use the weights you bring. --- # Guide: generate and edit images Canonical: https://popnow.ai/docs/guide-image-generation > Create images from text, edit photos with reference images, and use the background, upscale and cleanup tools — with model choices and costs. Images are the fastest and cheapest thing to generate, which makes them the best place to learn the API. Most image models finish in under a minute, so you can use `?wait=60` and get the picture back in the same response. ## Text to image [#text-to-image] ```bash curl -X POST "https://api.popnow.ai/api/v1/jobs/createTask?wait=60" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "model": "black-forest-labs/flux-2-klein-9b/text-to-image", "input": { "prompt": "Product photo of a ceramic coffee mug on a linen tablecloth, soft window light, shallow depth of field", "aspect_ratio": "4:3" } }' ``` The result is in `data.output.assets[0].url`. If the response is a `202` instead, the model needed more than 60 seconds; read the task with `recordInfo`. ### Which model [#choose-a-model] | You want | Try | Why | | ------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- | | The quickest, cheapest drafts | `alibaba/z-image-turbo/text-to-image` | About ten seconds and a couple of credits per image | | Good all-round quality at a low price | `black-forest-labs/flux-2-klein-9b/text-to-image` | Fast, sharp, follows prompts well | | Readable text, posters, diagrams | `google/nano-banana-2.1/text-to-image`, `openai/gpt-image-2/text-to-image` | Strong at lettering and layout; up to 4K | | Several options in one call | Models with `num_outputs` | One task, several images | Prices differ by model and option. [Models](https://popnow.ai/docs/models) lists the starting price of each; its reference page lists every tier. ## Edit with reference images [#edit] Editing models end in `/edit` and take the source pictures in `image_urls`, in the order your prompt refers to them: ```json { "model": "google/nano-banana-2.1/edit", "input": { "prompt": "Turn the scene into a bright summer afternoon and keep the composition", "image_urls": ["https://media.popnow.ai/movie/lighthouse.webp"] } } ``` * Say what must **stay the same** as well as what should change; editing models take "keep the face and the pose" literally. * With several references, name them by position: "put the jacket from the second image on the person in the first". * Local files go through [an upload](https://popnow.ai/docs/media#upload) first; reused files belong in your [asset library](https://popnow.ai/docs/assets). ## One-step tools [#tools] PopNow's own tools take a single image and need no prompt: | Model ID | Does | | ----------------------------------- | -------------------------------------------------- | | `popnow/background-remover-v1/edit` | Cuts the subject out onto a transparent background | | `popnow/image-upscaler-v1/upscale` | Enlarges and sharpens an image | | `popnow/object-eraser-v1/edit` | Removes an object you point out | | `popnow/image-expander-v1/edit` | Extends the picture beyond its edges | ```json { "model": "popnow/background-remover-v1/edit", "input": { "image_url": "https://media.popnow.ai/movie/lighthouse.webp" } } ``` ## Settings that matter [#settings] * **`aspect_ratio`** and **`resolution`** are the two settings that change price most often. Ask [jobs/quote](https://popnow.ai/docs/billing#quote) when in doubt. * **`seed`**: leave it out and a random one is chosen and returned in the task's `input`. Send that number back to get a close variation of a result you liked; a fixed seed keeps runs comparable but does not promise an identical picture. * **`output_format`**: `png` keeps transparency and detail; `jpeg` is smaller. * Unknown settings are rejected, so check the model's schema rather than copying settings from another model. ## Prompts that work [#prompts] Describe the picture the way a photographer or illustrator would brief it: subject, setting, composition, light, style. "A ceramic mug, top-down, on a linen cloth, morning window light, muted colours" beats "a nice mug photo". Put the most important thing first, and avoid long lists of style words that pull in different directions. ## Errors you may meet [#errors] * `content_rejected` — the model declined the prompt or the reference. Rephrase it or try another model. * `invalid_asset` — a reference could not be read. Check that the URL returns the image itself, or upload the file. * `unsupported_combination` — this model cannot do this mix of settings right now; change the one named in the message. All of them release the hold, so a failed image costs nothing. --- # Guide: turn images into video Canonical: https://popnow.ai/docs/guide-image-to-video > Animate a photo, control the first and last frame, use reference images, extend a clip and keep costs predictable. Video takes minutes, not seconds, so a video integration is built around **tasks and webhooks** rather than one waiting request. This guide walks through the common jobs with real model IDs. ## Animate one image [#image-to-video] ```bash 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": "alibaba/wan-3.0/image-to-video", "input": { "image_url": "https://media.popnow.ai/movie/lighthouse.webp", "prompt": "Waves roll in slowly, clouds drift, the camera pushes gently toward the lighthouse", "resolution": "480p", "duration_seconds": 5 }, "callBackUrl": "https://your-app.example.com/hooks/popnow" }' ``` You get a `202` with a `taskId` right away. A few minutes later the [webhook](https://popnow.ai/docs/webhooks) delivers the finished task, or you read it with `recordInfo`. The video is in `output.assets[0]`, with `durationSeconds`, `width` and `height`. ## What it costs [#cost] Most video models charge **per second of output**, and the rate rises with resolution. For `alibaba/wan-3.0/image-to-video` that is 6.75 credits per second at 480p, 13.5 at 720p and 27 at 1080p — so the request above holds 34 credits (5 × 6.75, rounded up) and costs the same when it succeeds. * Start at the lowest resolution and a short duration while you tune prompts, then raise them for the final render. * Ask [jobs/quote](https://popnow.ai/docs/billing#quote) for the exact price before showing it to a user. * A failed or expired video costs nothing. ## Choose the first and last frame [#first-last-frame] Many image-to-video models take `last_image_url` as well. The clip starts on `image_url` and ends exactly on `last_image_url`, which is how you make a transformation or a smooth loop (use the same image for both). ## Reference video: keep a character or product consistent [#reference-to-video] `reference-to-video` models build a new scene around one or more reference images (and, on some models, reference video or audio) instead of animating a given first frame: ```json { "model": "minimax/h3/reference-to-video", "input": { "prompt": "The same lighthouse at night, its beam sweeping over a calm sea while the camera circles slowly", "reference_image_urls": ["https://media.popnow.ai/movie/lighthouse.webp"], "duration_seconds": 6 } } ``` How each model refers to its references in the prompt differs; the field descriptions on its reference page say how. ## Make a clip longer [#extend] `video-extend` models continue an existing clip from its last frame. Send the clip as `video_url` and describe what happens next: ```json { "model": "minimax/h3/video-extend", "input": { "video_url": "https://media.popnow.ai/showcase/moon-picnic-loop.mp4", "prompt": "The camera keeps drifting right as the picnic blanket ripples in the breeze" } } ``` Extension models that join the clips return two assets: the joined video first (`role: "video"`), then the new part alone (`role: "segment"`). ## Sound [#audio] Models with `generate_audio` add a soundtrack to the clip; turning it off can lower the price on some models. To add sound to a video you already have, use `popnow/foley-v1/video-to-audio`, which returns the same video with generated sound. ## Prompts that work for video [#prompts] Describe **motion**, not just the scene: what moves, how fast, and what the camera does. "The camera slowly pushes in while the subject turns toward the light" gives the model something to do; "a beautiful lighthouse" does not. The picture already says what things look like — spend the words on what changes. ## A production pattern [#pattern] 1. Upload or save the source image, and keep its `popnow://` URI. 2. Quote, show the price, then `createTask` with an `Idempotency-Key` derived from your own job ID and a `callBackUrl`. 3. In the webhook, verify the signature, acknowledge, then download the video into your own storage. 4. Once a day, list recent tasks with [`GET /api/v1/jobs`](https://popnow.ai/docs/tasks#list) and read any whose webhook you never received. See [Production checklist](https://popnow.ai/docs/production-integration) for the rest. --- # Production checklist Canonical: https://popnow.ai/docs/production-integration > What to check before real users depend on your integration — keys, retries, webhooks, storage, costs and monitoring. A demo needs one request. A product needs to survive timeouts, duplicate webhooks, expired links and a key that leaks. Go through this list before launch. ## Keys [#keys] * [ ] The key lives in your server's secret store, never in a browser or app bundle. * [ ] Each environment and each service has its own key, so one can be revoked alone. * [ ] Production keys are limited to the models you use and have a daily spend cap. * [ ] You know how to rotate a key without downtime ([Authentication](https://popnow.ai/docs/authentication#rotation)). ## Creating tasks [#creating] * [ ] Every `createTask` sends an `Idempotency-Key` derived from your own job ID, generated once, reused on every retry. * [ ] Only `429`, `500`, `503` and `50301` are retried, with backoff and `Retry-After` ([Errors](https://popnow.ai/docs/errors#retry-code)). * [ ] `400`, `40004` and other "fix the request" errors are shown or logged, not retried. * [ ] If you show prices, you quote first and pass `quoteId` and `expectedCredits`, and handle `40901`. * [ ] A low balance (`40201`) reaches a person — an alert, an email, a banner — before users notice. ## Getting results [#results] * [ ] Tasks carry a `callBackUrl`; your endpoint verifies the signature and rejects stale timestamps. * [ ] The webhook handler answers `2xx` quickly and does the work in the background. * [ ] Duplicate deliveries are ignored by `request_id`. * [ ] A periodic sweep reads tasks whose webhook never arrived. * [ ] Your code treats any state other than `succeeded`, `failed`, `expired` and `canceled` as "still running". ## Files [#files] * [ ] Finished files are copied into your own storage (or your asset library) instead of linking to the signed URL, which expires in about 20 minutes. * [ ] You store `taskId`s, not signed links. * [ ] Your download code does not send the API key to the signed URL. * [ ] Reference files are public HTTPS links to the file itself, uploads, or assets — not share pages. ## Costs [#costs] * [ ] You know each model's price unit (per image, per second, per request) and the settings that raise it. * [ ] Previews and tests use the cheapest resolution and shortest duration. * [ ] A spend cap stops runaway loops before they empty the balance. * [ ] You check `credits` and `settled` rather than assuming the estimate was charged. ## Monitoring [#monitoring] * [ ] You log `request_id`, `taskId`, `code` and `errorCode` for every failure. * [ ] You watch the share of `failed` tasks per model; a sudden rise usually means a prompt, input or model change. * [ ] You track time from `createTask` to the webhook per model, and alert on outliers. * [ ] Someone on your team knows where the API console's logs and usage pages are. ## Content [#content] PopNow is a general-audience service. Models apply their own content rules, and requests they decline fail with `content_rejected`. If your users write prompts, tell them in your own interface why a request was declined instead of showing a raw error, and let them change it. --- # OpenAI Videos compatibility Canonical: https://popnow.ai/docs/openai-videos > Call PopNow video models with the request and response shapes of the OpenAI Videos API. If your code already speaks the OpenAI Videos API, you can point it at PopNow and keep most of it. The compatible endpoints live under `https://api.popnow.ai/v1` and share keys, limits, validation and credits with the native API. | OpenAI-style call | Does | | ----------------------------- | ------------------------------------------- | | `POST /v1/videos` | Create a video task | | `GET /v1/videos/{id}` | Read its state | | `GET /v1/videos/{id}/content` | Redirect (`302`) to the finished file | | `GET /v1/models` | List callable models in OpenAI's list shape | Only video, image and audio models are listed; chat and text endpoints are not part of the PopNow API. ## Create a video [#create] ```bash curl -X POST "https://api.popnow.ai/v1/videos" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "model": "alibaba/wan-3.0/image-to-video", "prompt": "Waves roll in slowly, the camera drifts toward the lighthouse", "input_reference": "https://media.popnow.ai/movie/lighthouse.webp", "seconds": "5", "size": "1280x720" }' ``` How the fields map onto the model's own input: | OpenAI field | Becomes | | ----------------------- | ------------------------------------------------------------------------------------- | | `model` | A video model ID from `/v1/models` | | `prompt` | `prompt` | | `input_reference` | `image_url` / `image_urls` (an HTTPS URL or a `popnow://` upload) | | `seconds` | `duration_seconds` | | `size` (`WIDTHxHEIGHT`) | `resolution` and/or `aspect_ratio`, or `width` / `height` when the model takes pixels | A field the chosen model does not support is rejected with `400`, never silently dropped. For settings OpenAI's shape has no field for, use the native [createTask](https://popnow.ai/docs/tasks). ## Read and download [#read] States map as `queued` → `queued`, `running` → `in_progress`, `succeeded` → `completed`, and `failed` / `expired` → `failed` with an `error` object. `GET /v1/videos/{id}/content` answers `302` to a short-lived signed link — follow redirects in your client — `409` while the video is still being made, and `404` if it failed. The returned objects never contain prices. Read the cost with the native [`recordInfo`](https://popnow.ai/docs/tasks#record-info), using the same ID. ## Errors [#errors] Errors use OpenAI's `{ "error": { "message", "type", "param", "code" } }` shape with the same HTTP status the native API would return. A content refusal is HTTP `400` with `code: "content_rejected"`, so you can tell it apart from a parameter error (`invalid_request` or `unsupported_parameter`, with `param` naming the field). --- # AI agents and MCP Canonical: https://popnow.ai/docs/mcp > Use the PopNow API from Claude, ChatGPT, Cursor and other agents today, and what the MCP server will add when it opens. AI assistants and coding agents can already create images, video and audio with PopNow: give them the API key and point them at the machine-readable docs. A ready-made **MCP server** — one address you add to Claude, ChatGPT, Cursor and other clients — is being prepared and is not open yet. The [MCP page](https://popnow.ai/mcp) shows the planned tools and will switch on when it launches. ## What to give an agent today [#today] | Resource | Address | Use | | ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------- | | OpenAPI | [`/docs/openapi.yaml`](https://popnow.ai/docs/openapi.yaml) | Every endpoint, field and error, for tool generators and agents that read specs | | Docs index | [`/docs/llms.txt`](https://popnow.ai/docs/llms.txt) | A short map of these docs and every model, written for language models | | Full docs | [`/docs/llms-full.txt`](https://popnow.ai/docs/llms-full.txt) | All pages in one Markdown file | | One page | `/docs/markdown/` | A single page as Markdown — the **Copy page** button on every page gives you the same | A prompt that works well in coding agents: ```text Use the PopNow API (OpenAPI: https://popnow.ai/docs/openapi.yaml, docs: https://popnow.ai/docs/llms.txt). My key is in the POPNOW_API_KEY environment variable; never print it or write it to a file. Before you create anything, call POST /api/v1/jobs/quote and tell me the price in credits (1 credit = $0.01). Send an Idempotency-Key with every createTask, and wait for the result with ?wait=60 or by polling recordInfo. ``` ## Keep the agent safe [#safety] * Give the agent its **own key**, restricted to the models it needs and with a daily spend cap ([Authentication](https://popnow.ai/docs/authentication#restrictions)). If something goes wrong, revoke that one key. * Ask it to quote before it runs anything expensive, especially video. * The key belongs in the environment, not in the conversation: agents sometimes echo what they are told. ## When the MCP server opens [#mcp-server] The MCP server will use the same API key and the same credits as everything on this site, and its tools will map one-to-one onto the endpoints documented here — choose a model, quote, create, wait for the result, download. Anything you build against the REST API today keeps working unchanged. --- # Credits and billing Canonical: https://popnow.ai/docs/billing > How API calls spend credits, how holds and final charges work, quotes before you run, balance, usage and spend caps. The API spends the **same credits** as popnow\.ai. There is no separate API plan, no subscription and no minimum: buy a [credit pack](https://popnow.ai/pricing) when you need more, and the free credits you received at sign-up work here too. * **1 credit = $0.01** at the base rate. Larger packs include bonus credits, so each credit costs you a little less. * Prices are per model and per option — per image, per second of video or audio, per request, or per 1,000 characters of speech. Each model's page lists them. * Responses give every amount twice: as whole credits (`credits`, `maxCredits`, `availableCredits` …) and as a US dollar string (`cost`, `maxCharge`, `available` …). Use the credit fields; the dollar strings are there for accounting. ## How one task is charged [#how-a-task-is-charged] `hold` → `settle` / `release` Credits are held when the task is accepted, then resolved exactly once: settled against actual usage, or released in full. 1. **Hold.** When `createTask` accepts a task, the highest price it can reach is set aside from your balance (`estimatedCredits`). Held credits cannot be spent twice, but they are not charged yet. 2. **Settle or release.** When the task finishes: * `succeeded` → you pay what it actually used, **rounded up to whole credits and never more than the hold**. The rest of the hold returns to your balance. * `failed` or `expired` → the whole hold is released. You pay nothing. The task record shows where it stands: while `settled` is `false`, `credits` is the hold; once `settled` is `true`, `credits` is the final charge. A few models charge for a request their maker refused on content grounds. When that applies, the model's page says so; everywhere else, failure is free. | Situation | Charged? | | ------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `createTask` rejected (`400`, `402`, `403`, `429`, `50301` …) | No — no task, no hold | | Task `succeeded` | Yes, the actual amount, capped at the hold | | Task `failed` or `expired` | No, except the content-refusal case above | | Your connection dropped after sending | Unknown until you check — resend with the same `Idempotency-Key` to find the task | | Reading tasks, models, balance or usage | Never | ## Ask the price first [#quote] `POST /api/v1/jobs/quote` takes exactly the body you would send to `createTask` and returns the price without creating anything or holding credits: ```bash curl -X POST "https://api.popnow.ai/api/v1/jobs/quote" \ -H "Authorization: Bearer $POPNOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "alibaba/wan-3.0/image-to-video", "input": {"image_url": "https://media.popnow.ai/movie/lighthouse.webp", "resolution": "720p", "duration_seconds": 5}}' ``` ```json { "code": 200, "msg": "success", "data": { "quoteId": "q_…", "model": "alibaba/wan-3.0/image-to-video", "maxCredits": 68, "expectedCredits": 68, "estimatedCost": "0.68", "maxCharge": "0.68", "currency": "USD", "quantity": "5", "unit": "per_second", "expiresAt": "2026-10-16T09:25:00Z" }, "request_id": "req_…" } ``` `maxCredits` is the most the request can cost — the amount that will be held. `expectedCredits` is what it usually costs; the two differ only when the final charge depends on the output, such as a clip's real length. To make sure the price does not change between showing it to a user and running the task, pass `quoteId` and `expectedCredits` (the `maxCredits` you showed) in `createTask` within five minutes. If anything changed, `createTask` answers `40901` and nothing is held; quote again. ## Balance [#balance] ```bash curl "https://api.popnow.ai/api/v1/chat/credit" \ -H "Authorization: Bearer $POPNOW_API_KEY" ``` `availableCredits` is what new tasks can use, `heldCredits` is set aside for running tasks, and `totalCredits` is both together (`available`, `held` and `total` give the same amounts in US dollars). The path name is historical; it has nothing to do with chat. When the available balance cannot cover a new task's hold, `createTask` returns `40201`. ## Usage [#usage] `GET /api/v1/usage?from=2026-10-01&to=2026-10-16` returns, for the key you call it with, the number of tasks, how many succeeded and failed, and what they cost in credits — in total (`totalCredits`), per day and per model. Dates are UTC, `to` is exclusive and the range is at most 92 days. Only settled charges count, so a day's total can still move while late tasks finish. This report is limited to 30 requests per minute; for the whole account, use the usage page in the API console. ## Spend caps [#spend-caps] Each key can have a daily, monthly and lifetime cap, set in the API console and shown in credits. A request that would go over a cap returns `40202` and holds nothing. Daily caps reset at 00:00 UTC, monthly caps on the 1st of the month at 00:00 UTC, and a lifetime cap never resets. Keys have no caps unless you add them. ## Buying credits and refunds [#buying] Credits are bought on the website, not through the API. Packs and prices are on the [pricing page](https://popnow.ai/pricing); the [refund policy](https://popnow.ai/refunds) explains what can be refunded and when. --- # API reference Canonical: https://popnow.ai/docs/api-reference > Every endpoint of the PopNow API on one page, with links to the full OpenAPI contract. The complete, machine-readable contract is [`openapi.yaml`](https://popnow.ai/docs/openapi.yaml) (OpenAPI 3.1). Import it into Postman, Insomnia or a code generator, or hand it to an AI agent. This page is the human-sized map. * **Base URL:** `https://api.popnow.ai/api/v1` (OpenAI-compatible routes: `https://api.popnow.ai/v1`) * **Authentication:** `Authorization: Bearer sk-popnow-…` on every request * **Envelope:** `{ "code", "msg", "data", "request_id" }`; `code: 200` is success * **Amounts:** whole credits (`credits`, `maxCredits` …), each also given as a US dollar string; 1 credit = $0.01 ## Tasks [#tasks] | Method and path | Does | Page | | ------------------------------ | ------------------------------------------------------- | ---------------------------------- | | `POST /jobs/createTask` | Create a generation task (optional `?wait=` up to 60 s) | [Tasks](https://popnow.ai/docs/tasks#create-a-task) | | `GET /jobs/recordInfo?taskId=` | Read a task's state and result | [Tasks](https://popnow.ai/docs/tasks#record-info) | | `GET /jobs` | List this key's tasks | [Tasks](https://popnow.ai/docs/tasks#list) | | `POST /jobs/quote` | Price a request without running it | [Billing](https://popnow.ai/docs/billing#quote) | | `POST /jobs/retry` | New task from a failed or expired one | [Tasks](https://popnow.ai/docs/tasks#retry) | | `POST /jobs/purge` | Delete a finished task's content | [Tasks](https://popnow.ai/docs/tasks#purge) | ## Models [#models] | Method and path | Does | Page | | --------------------- | ----------------------------------------------------------------------- | ---------------------------------- | | `GET /models` | Every callable model with prices (`?includeSchema=1` for input schemas) | [Models](https://popnow.ai/docs/models#catalog-api) | | `GET /models/{model}` | One model, including its input schema | [Models](https://popnow.ai/docs/models#catalog-api) | ## Files and assets [#files] | Method and path | Does | Page | | ------------------------------------------ | -------------------------------------------- | ------------------------------------ | | `POST /common/upload-url` | Ticket for a direct upload | [Media](https://popnow.ai/docs/media#upload) | | `POST /files/{fileId}/commit` | Finish an upload and get its `popnow://` URI | [Media](https://popnow.ai/docs/media#commit) | | `POST /files/import` | Import a LoRA file from a link | [Assets](https://popnow.ai/docs/assets#lora) | | `POST /common/download-url` | Fresh signed link to a task's output | [Media](https://popnow.ai/docs/media#download) | | `GET /assets`, `POST /assets` | List or save assets | [Assets](https://popnow.ai/docs/assets#asset-library) | | `GET`, `PATCH`, `DELETE /assets/{assetId}` | Read, rename or delete one asset | [Assets](https://popnow.ai/docs/assets#manage) | | `GET /assets/entities` | Your characters | [Assets](https://popnow.ai/docs/assets#manage) | | `GET /assets/usage` | Storage plan and usage | [Assets](https://popnow.ai/docs/assets#plans) | ## Account [#account] | Method and path | Does | Page | | ------------------ | ------------------------------------ | -------------------------------- | | `GET /chat/credit` | Balance: available, held, total | [Billing](https://popnow.ai/docs/billing#balance) | | `GET /usage` | This key's usage by day and by model | [Billing](https://popnow.ai/docs/billing#usage) | ## OpenAI Videos compatible (`/v1`) [#openai] | Method and path | Does | Page | | ----------------------------- | ---------------------------- | ----------------------------------------- | | `POST /v1/videos` | Create a video task | [OpenAI Videos](https://popnow.ai/docs/openai-videos) | | `GET /v1/videos/{id}` | Read it | [OpenAI Videos](https://popnow.ai/docs/openai-videos#read) | | `GET /v1/videos/{id}/content` | Redirect to the file | [OpenAI Videos](https://popnow.ai/docs/openai-videos#read) | | `GET /v1/models` | Model list in OpenAI's shape | [OpenAI Videos](https://popnow.ai/docs/openai-videos) | ## Common headers [#headers] | Header | Direction | Meaning | | ----------------------------------------------------------- | --------- | ----------------------------------------------------------------------------- | | `Idempotency-Key` | Request | Makes retries of a write safe for 24 hours ([Idempotency](https://popnow.ai/docs/idempotency)) | | `Accept-Language` | Request | Language of error messages ([Errors](https://popnow.ai/docs/errors#error-language)) | | `X-PopNow-Retention` | Request | Shorten how long one task's content is kept, in seconds | | `X-Request-Id` | Response | Same as `request_id`; quote it to support | | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After` | Response | Rate limits ([Rate limits](https://popnow.ai/docs/limits)) | --- # Errors Canonical: https://popnow.ai/docs/errors > Every business code, what it means, and which errors are worth retrying. When a call goes wrong, the response gives you a **number** (`code`) that says what kind of problem it is and a **sentence** (`msg`) for a person to read. Branch on the number; show or log the sentence. ## The envelope [#envelope] ```json { "code": 40201, "msg": "Insufficient balance", "request_id": "req_…" } ``` * `code` is a business code. `200` means success; everything else is listed below. * On failure there is no `data` field, except on a few [asset library](https://popnow.ai/docs/assets#errors) errors that add `data.reason`. * The HTTP status is set too (`40201` comes with HTTP `402`). Branch on one of the two, not a mix — we recommend `code`, which is more specific. * `request_id` is also in the `X-Request-Id` header. Include it when you contact support. If a response is not JSON at all — an HTML or plain-text page from the network in between — handle it by HTTP status and log the first part of the body. ## Error message language [#error-language] `msg` and a failed task's `errorMessage` are English by default. Send `Accept-Language` with one of `en`, `ja`, `ko`, `de`, `fr`, `es`, `pt-BR`, `ru`, `it` or `pl`, or set the API error language in the console, to get them in that language; other languages fall back to English. Error responses carry `Content-Language`. Codes never change with the language — another reason never to parse `msg`. When a model reports a specific reason for failing (for example an unreadable reference image), `errorMessage` passes that reason on in English, with internal details removed. ## General codes [#general-codes] | `code` | HTTP | Meaning | Retry? | | ------- | --------- | -------------------------------------------------------------------------------- | ---------------------------------------- | | `200` | 200 / 202 | Success | — | | `400` | 400 | The request or its `input` is invalid; `msg` names the problem | No — fix it | | `40003` | 400 | An upload does not match its ticket (size, type or content) | No — new ticket, upload again | | `40004` | 400 | Valid request, but the model cannot serve this combination of settings right now | No — change the setting named in `msg` | | `401` | 401 | Missing, unknown or revoked key ([details](https://popnow.ai/docs/authentication#errors)) | No | | `403` | 403 | Not allowed (see [Permissions](#permissions)) | No | | `404` | 404 | Does not exist, or belongs to another key | No | | `409` | 409 | Conflicts with the current state ([when to retry](#conflicts)) | Sometimes | | `40901` | 409 | A quote expired or the price changed | Quote again | | `413` | 413 | Request body too large | No — upload media instead of inlining it | | `429` | 429 | Too many requests | Yes — after `Retry-After` | | `500` | 500 | Something broke on our side | Yes — with backoff | | `503` | 503 | A supporting service is briefly unavailable | Yes — after `Retry-After` | ## Balance and caps [#balance] | `code` | HTTP | Meaning | What to do | | ------- | ---- | --------------------------------------- | --------------------------------------------------------- | | `40201` | 402 | Not enough credits for this task's hold | [Buy credits](https://popnow.ai/pricing), then send again | | `40202` | 402 | A key spend cap was reached | Raise the cap in the API console, or wait for it to reset | ## Permissions [#permissions] | `code` | HTTP | Meaning | What to do | | ------- | ---- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- | | `40301` | 403 | This key may not call this model | Add the model to the key's allowed list | | `40302` | 403 | Request came from an address outside the key's IP allowlist | Add the address or remove the restriction | | `40303` | 403 | The service is not offered in this region | Cannot be worked around | | `40310` | 403 | The account's email address is not verified yet | Open the link in your sign-up email; the key works as soon as it is verified | A plain `403` with a valid key means the account cannot use the API right now; contact support with the `request_id`. ## Model availability [#availability] | `code` | HTTP | Meaning | Retry? | | ------- | ---- | -------------------------------------------- | ------------------------------------ | | `50301` | 503 | The model cannot take tasks right now | Yes, later — or choose another model | | `40004` | 400 | The model works, but not with these settings | No — change the setting | Neither creates a task or holds credits. ## Task failure codes [#task-failure-codes] A task that was accepted and then failed has `state: "failed"` and one of these in `errorCode`. The list is closed: treat any other value as `upstream_failed`. | `errorCode` | Meaning | Retry? | | ------------------------- | ------------------------------------------------------------ | ------------------------------------------------ | | `invalid_request` | The model did not accept the parameters | No — fix them | | `unsupported_combination` | This combination of settings is not supported right now | No — change a setting | | `content_rejected` | The model declined this content | No — change the prompt or the reference | | `invalid_asset` | A reference file could not be read or is no longer available | Upload it again and create a new task | | `rate_limited` | The generation service was busy | Yes, with backoff | | `upstream_unavailable` | Generation is temporarily unavailable | Yes, shortly | | `generation_failed` | Generation ran but produced nothing | Maybe — the same input often fails the same way | | `timeout` | The task ran out of time | Maybe — try a shorter or lower-resolution output | | `upstream_failed` | Anything else | Yes, with backoff | A failed task releases its hold, so it costs nothing (a few models charge for content refusals, and say so on their page). `errorMessage` explains the reason in words; branch on `errorCode` only. ## When `409` is worth retrying [#conflicts] | Case | Retry? | | ------------------------------------------------------------------------ | ------------------------------------------- | | Two requests with the same `Idempotency-Key` arrived together | Yes, with the same key, after a short pause | | The `Idempotency-Key` was used for a different request or by another key | No | | An upload or an output is still being processed | Yes, after a few seconds | | `jobs/retry` on a task that has not finished yet | Yes, once it is final | | `jobs/retry` on a task that succeeded | No — create a new task | | An upload ticket or uploaded file has expired | No — upload again | ## Writing the retry [#retry-code] Retry only what can succeed on a second try, wait for `Retry-After` when it is given, and back off exponentially otherwise: ```javascript title="JavaScript" const RETRYABLE = new Set([429, 500, 503, 50301]); async function call(path, init, attempts = 4) { for (let i = 0; i < attempts; i++) { const res = await fetch(`https://api.popnow.ai/api/v1${path}`, init); if (!res.headers.get('content-type')?.includes('json')) { throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`); } const body = await res.json(); if (body.code === 200) return body.data; if (!RETRYABLE.has(body.code)) throw new Error(`${body.code} ${body.msg} (${body.request_id})`); const wait = Number(res.headers.get('retry-after')) * 1000 || 2 ** i * 1000 + Math.random() * 500; await new Promise((r) => setTimeout(r, wait)); } throw new Error('out of retries'); } ``` ```python title="Python" import random, time, requests RETRYABLE = {429, 500, 503, 50301} def call(method, path, attempts=4, **kwargs): for i in range(attempts): res = requests.request(method, f"https://api.popnow.ai/api/v1{path}", timeout=60, **kwargs) try: body = res.json() except ValueError: raise RuntimeError(f"HTTP {res.status_code}: {res.text[:200]}") if body["code"] == 200: return body["data"] if body["code"] not in RETRYABLE: raise RuntimeError(f'{body["code"]} {body["msg"]} ({body.get("request_id")})') retry_after = res.headers.get("retry-after") time.sleep(float(retry_after) if retry_after else 2**i + random.random() / 2) raise RuntimeError("out of retries") ``` When retrying `createTask`, always send the **same** `Idempotency-Key`, so a request that actually succeeded is not run and charged twice. ## Debugging [#debugging] 1. Note the `request_id` and the `code`. 2. Look the task up in the API console's logs, by task ID or `request_id`. 3. Still stuck? [Contact support](https://popnow.ai/contact) with the `request_id`, the time (UTC) and the `code`. Never send your API key. --- # Rate limits Canonical: https://popnow.ai/docs/limits > How many requests you can send, what happens when you send more, and the other limits worth knowing. Limits protect everyone sharing the service. The defaults are generous for normal use; if you hit one, the response tells you exactly how long to wait. ## At a glance [#at-a-glance] | Limit | Default | When you hit it | | ------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------ | | Writes per account (`createTask`, `retry`, uploads …) | 1,000 per 10 seconds | `429` with `Retry-After` | | Reads per account (`recordInfo`, `models`, `jobs`, `quote` …) | 1,000 per 10 seconds | `429` with `Retry-After` | | Upload tickets | 1,000 per 10 seconds, on top of writes | `429` with `Retry-After` | | Usage report (`/api/v1/usage`) | 30 per minute per account | `429` with `Retry-After` | | Asset library | 60 reads and 30 saves per minute, plus a daily number of new assets | `429` with `Retry-After` | | Any request, before the key is checked | 6,000 per minute per network | `429` | | Request body | 2 MiB of JSON | `413` | | Balance | What is in your account | `40201` | | Key spend caps | None unless you set one | `40202` | Limits count per **account**, not per key — creating more keys does not raise them. ## Response headers [#headers] | Header | Meaning | | ----------------------- | ---------------------------------------------------------------------------------- | | `X-RateLimit-Limit` | Your allowance for the current window | | `X-RateLimit-Remaining` | Requests left in it — sent on successful responses too, so you can slow down early | | `Retry-After` | On `429`: seconds to wait before trying again | The numbers above are defaults and can change; trust the headers rather than hard-coding them. ## What a limit looks like [#response] ```json { "code": 429, "msg": "Too many requests. Retry in a moment", "request_id": "req_…" } ``` A request over the limit is rejected immediately — not queued — and costs nothing. ## Handling it [#handling] * Wait at least `Retry-After` seconds, then retry. Add a little random jitter so a batch of clients does not come back at the same instant. * When you retry `createTask`, send the **same** `Idempotency-Key` ([Idempotency](https://popnow.ai/docs/idempotency)). * Poll gently: every few seconds for images, every 10–15 seconds for video. Better still, use [webhooks](https://popnow.ai/docs/webhooks) — they cost no requests at all. ## Running many tasks at once [#concurrency] Rate limits cap how fast you **send** requests, not how many tasks run at the same time. You can have many tasks in flight; long video jobs simply take longer. If you need to start thousands of tasks, spread them out and let webhooks bring the results back. Need a higher allowance for a launch or a large batch? [Contact us](https://popnow.ai/contact) with your expected volume. --- # Troubleshooting Canonical: https://popnow.ai/docs/troubleshooting > Fixes for the problems people meet most often, from a 401 on the first call to a webhook that never arrives. Find the symptom, try the fix. If nothing here helps, contact [support](https://popnow.ai/contact) with the `request_id`. ## The first call fails [#first-call] **`401` "Missing credentials".** The `Authorization` header did not arrive. Check that the environment variable is set in the shell or process that runs the request (`echo ${POPNOW_API_KEY:0:10}` should print `sk-popnow-`), and that a proxy or HTTP library is not dropping the header. **`401` "Key format is not recognized".** The value is not a PopNow key — often a copy of something else from the clipboard. Keys start with `sk-popnow-`. **`401` "Credentials are invalid or expired".** The key was revoked, expired, or mistyped. Create a new one in the [API console](https://popnow.ai/api/console/keys). **`40310`.** Your account's email address is not verified. Open the link from your sign-up email; the key starts working immediately afterwards. **`40201` "Insufficient balance".** The account does not have enough credits for this task's hold. Video holds can be large; check the price with [jobs/quote](https://popnow.ai/docs/billing#quote) and [buy credits](https://popnow.ai/pricing). ## The request is rejected [#rejected] **`400` naming a field.** The `input` does not match the model's schema: a misspelt field, a value outside the allowed list, or a missing required field. Open the model's reference page from [Models](https://popnow.ai/docs/models) and compare. **`400` "Invalid callback URL".** The `callBackUrl` must be public HTTPS, on port 443, with no username or password. `localhost` and private addresses are refused. **`40004`.** The model is working but cannot serve this exact combination of settings right now. Change the setting named in `msg` (often resolution, duration or aspect ratio). **`50301`.** The model is temporarily unable to take tasks. Retry later or pick another model. **`413`.** The JSON body is over 2 MiB, usually because of inline images. [Upload](https://popnow.ai/docs/media#upload) them instead. ## The task fails [#task-fails] **`invalid_asset`.** A reference file could not be fetched or read. Open the URL in a private browser window: it must download the file itself, without a login. Share pages from cloud drives do not work. **`content_rejected`.** The model declined the prompt or a reference image. Rephrase, use different references, or try another model. **`timeout` or `expired`.** The job took longer than allowed. Try a shorter duration or lower resolution. **`generation_failed`.** The model ran but produced nothing. The same input often fails again; change the prompt or the settings. Failed and expired tasks do not use credits. ## The result [#result] **The download link returns `403` or "expired".** Signed links last about 20 minutes. Read the task again with `recordInfo` for a fresh link. **The download link fails when I send my API key.** Do not send `Authorization` to the signed link; the link itself is the permission. **`recordInfo` returns `404` for a task I created.** Tasks are visible only to the key that created them. Use the same key, or look the task up in the API console. ## Webhooks [#webhooks] **Nothing arrives.** Check that the URL is reachable from the internet over HTTPS, that your firewall allows it, and that your handler answers `2xx` within 15 seconds. Failed deliveries are listed in the API console. **The signature never matches.** Hash the **raw** body bytes, not a re-serialised JSON object; use `data.taskId`, the `X-Webhook-Timestamp` header, and the secret you generated most recently. **The same task arrives twice.** That is expected after a retry. Ignore duplicates by `request_id`. ## Uploads [#uploads] **The `PUT` returns `403` from storage.** A header differs from the ticket. Send `Content-Type` and `Content-Length` exactly as given, and no `Authorization` header. **Commit returns `40003`.** The file you uploaded does not match what you declared. Request a new ticket with the right `contentType` and `bytes`. **Commit returns `409`.** The ticket expired (after about 20 minutes). Request a new one right before uploading. --- # FAQ Canonical: https://popnow.ai/docs/faq > Short answers about accounts, credits, models, limits, content and support for the PopNow API. ## Do I need a separate plan for the API? [#plan] No. The API uses your PopNow account and the same credits as the website. There is no subscription and no minimum; free sign-up credits work too. ## How much does a call cost? [#cost] It depends on the model and its settings — per image, per second of video or audio, per request, or per 1,000 characters of speech. Every model's reference page lists its prices in credits (1 credit = $0.01), and [jobs/quote](https://popnow.ai/docs/billing#quote) gives the exact amount for one request. Failed tasks cost nothing. ## Are API prices the same as on the website? [#same-price] Yes. The same model with the same settings costs the same number of credits whichever way you run it. ## Which models can I call? [#models] Every image, video and audio model listed on [Models](https://popnow.ai/docs/models). Chat and text models are not available through the API. ## Can I call the API from a browser or mobile app? [#browser] Not directly. Anyone who can read your key can spend your credits, so keep it on your server and let your app talk to your server. ## How long are results kept? [#retention] Each task's record says exactly when its files will be removed (`retention.outputsExpireAt`). Signed download links themselves last about 20 minutes and can be refreshed by reading the task again. Copy anything you want to keep to your own storage or your [asset library](https://popnow.ai/docs/assets), or delete a task's content yourself with [purge](https://popnow.ai/docs/tasks#purge). ## Do you use my prompts or files to train models? [#training] No. See the [privacy policy](https://popnow.ai/privacy) for what is stored and for how long. ## Can I cancel a task? [#cancel] No. Once a task is accepted it runs to a final state. If it fails or expires, its hold is released. ## Is there a sandbox or test mode? [#sandbox] There is no separate sandbox. Use the cheapest models and the lowest settings while you build — many images cost one or two credits — and try models in the Playground on their API pages first. ## Is there an SDK? [#sdk] Not yet. The API is plain REST with JSON, so `fetch`, `requests` or any HTTP client works, and [`openapi.yaml`](https://popnow.ai/docs/openapi.yaml) can generate a typed client in most languages. ## What content is allowed? [#content] PopNow is a general-audience service; the [terms of service](https://popnow.ai/terms) apply to API use as well. Models also apply their own rules, and requests they decline fail with `content_rejected` at no cost. ## How do I get help? [#help] [Contact support](https://popnow.ai/contact) or email [support@popnow.ai](mailto:support@popnow.ai) with the `request_id` of the call, the time (UTC) and the `code`. Never send your API key.