Programmatic access to every AI image & video model. Same balance as your WOWFLOW account. Bearer-token auth, JSON in / JSON out, no SDK required.
All endpoints require an API key in the Authorization header:
Authorization: Bearer wf_live_...
Generate keys in Settings → API Access. Each account can have up to 10 active keys. The secret value is shown only once at creation time — store it somewhere safe.
https://wowflow.space/api/v1
curl https://wowflow.space/api/v1/models \
-H "Authorization: Bearer $WOWFLOW_KEY"
import os, requests
r = requests.get(
"https://wowflow.space/api/v1/models",
headers={"Authorization": f"Bearer {os.environ['WOWFLOW_KEY']}"},
)
print(r.json())
const r = await fetch("https://wowflow.space/api/v1/models", {
headers: { Authorization: `Bearer ${process.env.WOWFLOW_KEY}` },
});
console.log(await r.json());
Response:
{
"data": [
{
"id": "nano-banana-pro-1k",
"name": "Nano Banana Pro 1K",
"type": "image",
"price_cents": 11,
"price_usd": 0.11,
"resolution": "1K",
"duration_sec": null
},
...
]
}
Body:
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | id from /models |
prompt | string | yes | Text description. Seedream requires at least 3 characters. |
input | object | no | Model parameters — see 4.1 below |
| any other key | any | no | Merged into input. Putting aspect_ratio beside prompt and putting it inside input are equivalent. |
input:
image, images, image_url, image_urls,
input_urls, reference_image, reference_images,
refs. They are normalised to whichever field the underlying provider
reads, so you never have to match its naming. Images must be publicly reachable over
HTTPS.{"model":"seedream-5-pro-edit-2k","prompt":"add a red hat","image":"https://.../base.png"}
Everything in this section goes in input (or at the top level — they
are merged). Parameters that set the price are pinned by the model id and are
ignored if you send them: quality, resolution,
duration, sound, mode. Choose the tier by
choosing the model id, not by sending a parameter.
| Models | Reference input | Other parameters |
|---|---|---|
seedream-5-pro-1k / -2k |
— (text to image) | aspect_ratio: 1:1 4:3 3:4 16:9 9:16 2:3 3:2 21:9 (default 1:1)output_format: png | jpegnsfw_checker: boolean |
seedream-5-pro-edit-1k / -2k |
required, up to 10 images | Same as above |
gpt-image-2-1k / -2k / -4k |
optional — only the first image is used | aspect_ratio: auto 1:1 3:2 2:3 16:9 9:16 4:3 3:4 21:9size: explicit WIDTHxHEIGHT, overrides aspect_ratio. Both sides must divide by 16 and the ratio must sit between 1:3 and 3:1. |
nano-banana-pro-1k / -2k / -4k |
optional, up to 8 images | aspect_ratio: 1:1 3:2 2:3 16:9 9:16 4:3 3:4 21:9 |
kling-3-pro-*, kling-3-standard-* |
optional — first frame, or [first, last] |
Resolution, duration and audio are pinned by the model id |
kling-motion-control-720p / -1080p |
required — one character image, plus a motion reference video in video / video_urls |
Both the image and the video are mandatory |
seedance-2-* |
optional, up to 9 reference images | first_frame_url, last_frame_url, reference_video_urls, reference_audio_urls |
happyhorse-720p-10s |
— (text to video) | — |
502 upstream_error with
its message attached, and your balance is refunded automatically — a rejected
request never costs anything.
curl -X POST https://wowflow.space/api/v1/generations \
-H "Authorization: Bearer $WOWFLOW_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana-pro-1k",
"prompt": "a misty forest at dawn, soft light, cinematic"
}'
import os, requests
r = requests.post(
"https://wowflow.space/api/v1/generations",
headers={
"Authorization": f"Bearer {os.environ['WOWFLOW_KEY']}",
"Content-Type": "application/json",
},
json={
"model": "nano-banana-pro-1k",
"prompt": "a misty forest at dawn, soft light, cinematic",
},
)
gen = r.json()
print(gen["id"], gen["status"])
const r = await fetch("https://wowflow.space/api/v1/generations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WOWFLOW_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "nano-banana-pro-1k",
prompt: "a misty forest at dawn, soft light, cinematic",
}),
});
const gen = await r.json();
console.log(gen.id, gen.status);
Response (201):
{
"id": "8c2e90b1-...",
"status": "processing",
"model": "nano-banana-pro-1k",
"cost_cents": 11,
"cost_usd": 0.11,
"created_at": "2026-05-08T14:23:11.000Z"
}
Poll every 2–5 seconds until status is completed or failed. Webhooks are coming in v2.
curl "https://wowflow.space/api/v1/generations?id=$ID" \
-H "Authorization: Bearer $WOWFLOW_KEY"
import time, os, requests
def wait_for(gen_id, timeout=300):
headers = {"Authorization": f"Bearer {os.environ['WOWFLOW_KEY']}"}
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(
f"https://wowflow.space/api/v1/generations?id={gen_id}",
headers=headers,
).json()
if r["status"] == "completed":
return r["result_url"]
if r["status"] == "failed":
raise Exception(r.get("error") or "failed")
time.sleep(3)
raise TimeoutError("Generation timed out")
async function waitFor(id, timeoutMs = 300_000) {
const headers = { Authorization: `Bearer ${process.env.WOWFLOW_KEY}` };
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const r = await fetch(
`https://wowflow.space/api/v1/generations?id=${id}`,
{ headers }
).then(x => x.json());
if (r.status === "completed") return r.result_url;
if (r.status === "failed") throw new Error(r.error || "failed");
await new Promise(r => setTimeout(r, 3000));
}
throw new Error("Generation timed out");
}
Response when ready:
{
"id": "8c2e90b1-...",
"status": "completed",
"model": "nano-banana-pro-1k",
"result_url": "https://.../result.png",
"cost_cents": 11,
"cost_usd": 0.11,
"created_at": "2026-05-08T14:23:11.000Z",
"completed_at": "2026-05-08T14:23:32.000Z"
}
result_url is not
guaranteed to be permanent: for the video models and Seedream it points at the
provider's CDN, which deletes generated files after roughly two weeks. Fetch the bytes
and store them on your side in the same job that polls for the result — do not
treat the URL as durable storage.
Omit id to list your account's most recent generations (up to 200).
{
"balance_cents": 1234,
"balance_usd": 12.34,
"total_topup_cents": 5000,
"total_spent_cents": 3766,
"currency": "USD"
}
Returns recent spend + top-up transactions, ordered newest-first.
All errors follow this shape:
{ "error": { "code": "insufficient_balance", "message": "..." } }
| HTTP | code | Meaning |
|---|---|---|
| 400 | missing_model / missing_prompt / unknown_model | Bad request |
| 401 | missing_api_key / invalid_api_key | Auth failure |
| 402 | insufficient_balance | Top up to continue |
| 404 | not_found | Generation id doesn't belong to your account |
| 502 | upstream_error | Provider rejected the request (you were refunded) |
This is v1. Breaking changes will be released under a new path (/api/v2/...). Additive changes — new models, new fields — may happen at any time on v1.
result_url never expiresQuestions? Ping @fukfaku on Telegram.