Video generation
OpenAI-compatible asynchronous Videos API — submit, poll, and download for text, image, reference, and first/last-frame generation.
Video generation uses asynchronous tasks. Submission immediately returns a task ID; poll until completion, then download. The three endpoints follow OpenAI semantics and serve all video models in the catalog.
| Operation | Endpoint |
|---|---|
| Submit | POST /v1/videos |
| Query status and video download URL | GET /v1/videos/{video_id} |
| Download | GET /v1/videos/{video_id}/content |
Queries and downloads do not require the model name again: the task ID already identifies its model. One workflow supports all video models.
Complete workflow
This skeleton is shared by all modes. Change only create() arguments; polling and downloading stay the same.
import time
from openai import OpenAI
client = OpenAI(
base_url="https://api.goodputai.cn/v1",
api_key="YOUR_GOODPUT_API_KEY",
)
def generate(**kwargs) -> str:
"""Submit, poll, download; return the saved path. Shared by all modes."""
video = client.videos.create(**kwargs)
print(video.id, video.status) # video_xxx queued
deadline = time.time() + 15 * 60 # Overall timeout; do not poll indefinitely
while video.status in ("queued", "in_progress"):
if time.time() > deadline:
raise TimeoutError(f"Timed out before completion: {video.id}")
time.sleep(5)
video = client.videos.retrieve(video.id)
if video.status != "completed":
raise RuntimeError(f"Generation failed: {video.error}")
# Completed h3 / h3-fast responses also provide a direct download or playback URL
video_url = getattr(video, "video_url", None)
print("Video download URL:", video_url)
print("Link expiry:", getattr(video, "video_url_expires_at", None))
path = f"{video.id[:16]}.mp4"
client.videos.download_content(video.id).write_to_file(path)
print("usage:", video.usage) # Billing basis
return path
# Minimal example: text to video
generate(
model="wan/wan3.0-video",
prompt="Sunrise by the sea, waves breaking on rocks, cinematic camera movement",
size="1280x720",
seconds="5",
)# 1) Submit; use the id in the response for subsequent status queries
curl https://api.goodputai.cn/v1/videos \
-H "Authorization: Bearer $GOODPUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "wan/wan3.0-video",
"prompt": "Sunrise by the sea, waves breaking on rocks",
"size": "1280x720",
"seconds": "5"
}'
# 2) Query status and download URL until status becomes completed or failed
curl https://api.goodputai.cn/v1/videos/{video_id} \
-H "Authorization: Bearer $GOODPUT_API_KEY"
Providers with direct links return these fields on completion. Both minimax/h3 and minimax/h3-fast support them:
{
"id": "video_xxx",
"status": "completed",
"video_url": "https://example.com/generated-video.mp4?...",
"video_url_expires_at": null
}# 3a) Copy the complete video_url from the status response; no API Key is needed
VIDEO_URL="REPLACE_WITH_COMPLETE_VIDEO_URL_FROM_RESPONSE"
curl -fL "$VIDEO_URL" -o output.mp4
# 3b) If video_url is absent or null, use the download endpoint instead (always supported)
curl https://api.goodputai.cn/v1/videos/{video_id}/content \
-H "Authorization: Bearer $GOODPUT_API_KEY" \
-o output.mp4Get a video URL from task status
Once a task completes, GET /v1/videos/{video_id} returns video_url when the provider supplies a direct link. Both minimax/h3 and minimax/h3-fast support this workflow:
{
"id": "video_xxx",
"status": "completed",
"video_url": "https://example.com/generated-video.mp4?...",
"video_url_expires_at": null
}| Field | Meaning |
|---|---|
video_url | Direct link for a completed task; null while unfinished, on failure, or when the provider has no direct link |
video_url_expires_at | Link expiry as a Unix timestamp in seconds; null when the expiry cannot be determined |
expires_at | Task or result retention deadline, which may differ from link expiry |
Use video_url to download or play the result, and save the video before the link expires. A null expiry does not mean the link is permanent. Do not treat expires_at as the exact expiry of the direct link.
Status queries and /content downloads still require your GoodPut API Key. Do not send that Key to the video storage host when opening a direct link. Anyone holding the complete link may be able to access the video, so keep it private.
The Python SDK exposes these extension fields through getattr. Handle absent or null fields and fall back to the existing download endpoint:
video = client.videos.retrieve(video_id)
if video.status != "completed":
raise RuntimeError(f"Task is not complete: {video.status}")
video_url = getattr(video, "video_url", None)
if video_url:
print("Video URL:", video_url)
print("Link expiry:", getattr(video, "video_url_expires_at", None))
else:
client.videos.download_content(video.id).write_to_file("output.mp4")An absent or null video_url does not mean generation failed; use status to determine completion. /v1/videos/{video_id}/content remains available.
Four generation modes
Your inputs determine the mode; no extra mode parameter is required.
| Mode | Trigger | Typical use |
|---|---|---|
| Text to video | prompt only | Generate from scratch |
| Image to video | input_reference or media.first_frame | Animate an image |
| Reference to video | media.reference_image / reference_video / reference_audio | Preserve a subject, style, or voice |
| First/last frames | media.first_frame + media.last_frame | Specify the start and end; let the model generate the transition |
With the official OpenAI SDK, pass media through extra_body. media is not in the videos.create() signature, which accepts prompt, input_reference, model, seconds, and size. Passing media=[...] directly raises unexpected keyword argument. The SDK merges extra_body into the request body, so use:
extra_body={"media": [{"type": "reference_image", "url": "..."}]}With curl or requests sending JSON directly, media is an ordinary top-level field and needs no wrapper.
Models differ in how modes are selected:
- Wan splits modes into model IDs:
wan2.7-t2vis text-only,wan2.7-i2vimage-only, andwan2.7-r2vreference-only. Choosing the wrong model returns400.wan3.0-videosupports all four modes in one model. minimax/h3handles all four modes in one model, selecting from the inputs automatically.minimax/h3-fastuses the same calling convention asminimax/h3, but supports only1344x768/768x1344,secondsfrom 5~15, andseed. It is substantially faster, around 18 seconds for a 5-second video, making it suitable for rapid iteration. See the parameter table below.
Check each model’s detail page for supported modes.
Text to video
generate(
model="wan/wan3.0-video", # Or wan/wan2.7-t2v, wan/wan2.6-t2v
prompt="Walk forward along a neon street on a rainy night, first-person view",
size="720x1280", # Short side 720 → 720P tier; 9:16 → portrait
seconds="5",
prompt_extend=False, # ⚠️ Defaults to true and rewrites your prompt
)generate(
model="minimax/h3",
prompt="Walk forward along a neon street on a rainy night, first-person view. No subtitles or text.",
size="768x1344", # 9:16 portrait; see the six canvases below
seconds=6, # Integer from 4~15
)Image to video
generate(
model="wan/wan2.7-i2v", # Or wan/wan3.0-video
prompt="The camera slowly pulls back",
# Put URLs / base64 in extra_body (see the explanation below)
extra_body={"input_reference": "https://example.com/first.jpg"},
size="1280x720", # Sets the tier and unit price; ⚠️ actual aspect ratio follows the image
seconds="5",
)Wan input_reference accepts only public URLs or data:image/...;base64,... strings, not file uploads.
The official OpenAI SDK’s input_reference= argument accepts file objects only; a string fails locally. Put a URL or base64 string in extra_body, as above. With direct JSON over curl / requests, input_reference is an ordinary top-level field.
# Option one: public URL or inline data: URL, via extra_body
generate(
model="minimax/h3",
prompt="The camera slowly pulls back as the person turns naturally",
extra_body={"input_reference": "https://example.com/first.jpg"},
size="768x768",
seconds=4,
)
# Option two: upload a local file directly (supported by h3, not wan)
with open("first.jpg", "rb") as f:
generate(
model="minimax/h3",
prompt="The camera slowly pulls back as the person turns naturally",
input_reference=f,
size="768x768",
seconds=4,
)Reference to video
Reference assets guide subject, style, or voice rather than acting as the first frame.
generate(
model="wan/wan2.7-r2v", # Or wan/wan3.0-video
prompt="The same character walking through snow",
extra_body={
"media": [
{"type": "reference_image", "url": "https://example.com/character.jpg"},
]
},
size="1280x720",
seconds="5",
)wan2.7-r2v has no input_reference parameter. Pass reference assets through media only.
# h3 accepts multiple reference images in one request
generate(
model="minimax/h3",
prompt="In this room, a woman enters from the left and opens a chest",
extra_body={
"media": [
{"type": "reference_image", "url": "https://example.com/room.jpg"},
{"type": "reference_image", "url": "https://example.com/person.jpg"},
]
},
size="1344x768",
seconds=4,
)Multiple reference images have no named binding. You can supply several images, but cannot declare “this image is this character”. Cross-shot consistency depends on the model retaining the references; parameters do not guarantee it.
First and last frames
generate(
model="wan/wan3.0-video",
prompt="Transition from day to night",
extra_body={
"media": [
{"type": "first_frame", "url": "https://example.com/day.jpg"},
{"type": "last_frame", "url": "https://example.com/night.jpg"},
]
},
size="1280x720",
seconds="5",
)generate(
model="minimax/h3",
prompt="The person moves naturally and continuously from the starting pose to the ending pose",
extra_body={
"media": [
{"type": "first_frame", "url": "https://example.com/start.jpg"},
{"type": "last_frame", "url": "https://example.com/end.jpg"},
]
},
size="1344x768",
seconds=4,
)H3 cannot mix first/last frames with reference assets. first_frame / last_frame and reference_* use different internal generation paths; mixing them returns 400. Supply one kind or the other.
At most one first frame and one last frame are allowed. Their array order does not matter; the gateway orders them automatically.
Task status
GET /v1/videos/{video_id} returns four status values. completed and failed are terminal:
| Status | Meaning | Action |
|---|---|---|
queued | Waiting to start | Keep polling |
in_progress | Generating | Keep polling |
completed | Finished | Download or play video_url; fall back to /content if no direct URL is available |
failed | Generation failed | Read error.code / error.message; not charged |
On completion, the download URL is in the video_url field of this status response. Both minimax/h3 and minimax/h3-fast support it. video_url_expires_at is the link expiry in Unix seconds, or null when unknown. Direct links do not require your GoodPut API Key; save the video before expiry. See Get a video URL from task status for the response and usage.
Do not poll indefinitely. Set an overall timeout and a maximum poll count; treat expiry as a failure and alert, so an upstream problem does not leave your process hanging.
Measured reference timings: a 5-second Wan 480P video takes about 1–2 minutes; h3 takes around 1 minute for 4 seconds and 1.5 minutes for 6 seconds; h3-fast takes about 18 seconds for a 5-second text-to-video task, or about 1 minute with reference assets. Start with a 5-second poll interval and allow an overall timeout above 10 minutes.
Aspect ratio and duration
Wan and h3 use different rules. Read the tab for your model.
size controls two things: its short side sets the resolution tier and therefore the per-second price, while its aspect ratio sets the frame shape.
| Short side | Tier |
|---|---|
| 480 | 480P |
| 540 | 540P |
| 720 | 720P |
| 1080 | 1080P |
1920x1080 and 1080x1920 have the same tier and unit price. Use the short side to estimate costs.
Five ratios are supported: 16:9 landscape, 9:16 portrait, 1:1 square, 4:3, and 3:4. Specify the tier number as the short side and choose the long side for the desired ratio:
| Ratio | 480P | 540P | 720P | 1080P |
|---|---|---|---|---|
16:9 landscape | 832x480 | 960x540 | 1280x720 | 1920x1080 |
9:16 portrait | 480x832 | 540x960 | 720x1280 | 1080x1920 |
1:1 square | 480x480 | 540x540 | 720x720 | 1080x1080 |
4:3 | 640x480 | 720x540 | 960x720 | 1440x1080 |
3:4 | 480x640 | 540x720 | 720x960 | 1080x1440 |
1280x720, 1280*720, and 1280×720 are all accepted.
Unsupported combinations fail rather than being silently replaced.
| Input | Result |
|---|---|
1024x1024, 800x600: short side is not a tier number | 400, listing valid short sides |
2560x1080: matches 1080P short side but uses unsupported 21:9 | 400, listing supported ratios |
1920x1080 for a model supporting only 480P | 400 (code: unsupported_value), listing the model’s available tiers |
Output dimensions may differ from size. The upstream computes dimensions from the tier’s target pixel count and ratio, with both dimensions divisible by 16. For 480P, 16:9 produces 832×480, while 1:1 produces 624×624. These actual dimensions can also be passed back as size.
Image-to-video follows the input image’s aspect ratio, not size. In a measured example, a 512×512 image with size="832x480" produces 624×624. size still determines tier and unit price; the image determines shape. Supply a landscape image for landscape output.
Duration: allowed seconds values depend on the model; check its detail page. Use a string such as "5" with the OpenAI SDK.
H3 has no resolution-tier ladder, only six fixed canvases. The short side is 768, except 21:9, which uses 672 because of the pixel limit. size must exactly match one:
| Ratio | size |
|---|---|
21:9 | 1536x672 |
16:9 landscape | 1344x768 |
4:3 | 1024x768 |
1:1 square | 768x768 |
3:4 | 768x1024 |
9:16 portrait | 768x1344 |
Any other value returns 400, listing all six. There is no silent substitution with the nearest canvas.
All six canvases have the same price. H3 is charged per second, independent of resolution.
Duration: seconds must be an integer from 4~15, not a fraction.
Output is slightly longer than requested, but billing uses the requested duration. The model’s duration snaps upward to a fixed frame-count grid: 4 seconds produces about 4.46 seconds, and 6 produces about 6.58 seconds. The extra duration is free, with a difference up to about 0.67 seconds.
seconds=8 is the only whole-second output in the 4~15 range, producing 8.00 seconds. Prefer it when duration must be exact.
H3 always generates an audio track, with no off switch, because video and audio share the same denoising process; disabling playback would not save computation. Remove the track afterward if needed: ffmpeg -i in.mp4 -c:v copy -an out.mp4. H3-fast uses the same model and audio behavior.
H3-fast uses the same model as minimax/h3 on another set of machines. It is much faster but enables only two canvases:
| Ratio | size |
|---|---|
16:9 landscape | 1344x768 (default) |
9:16 portrait | 768x1344 |
Omitting size uses 1344x768. All other canvases, including h3’s four additional ones such as 768x768, return 400 with no silent substitution.
Both canvases have the same unit price, charged per second only.
Duration: seconds must be an integer from 5~15, not a fraction.
Output is slightly longer than requested and billed on requested seconds, except for seconds=15.
As in h3, duration snaps upward to the frame-count grid: 5 seconds produces about 5.17 seconds and 13 about 13.67 seconds. The extra duration is free, up to about 0.67 seconds. seconds=8 is the only exact whole-second output, at 8.00 seconds.
seconds=15 is the sole exception. The model can produce at most 14.375 seconds: the next grid point is 15.083 seconds, above the model’s 15-second limit. A request for 15 therefore generates at the 14-second setting, about 14.38 seconds, and is billed for 14 seconds. You are not charged for the missing duration. If duration matters, explicitly request 14 rather than 15.
Only h3-fast supports seed among the h3 variants. The same inputs and seed generally preserve composition, without guaranteeing pixel-identical output. Omit it for random output. With the official OpenAI SDK, use extra_body={"seed": 42}.
Parameters
Supported parameters differ by model. Passing a parameter unsupported by that model returns 400 with an explanation; it is not silently ignored, so you do not pay for a result generated with unexpectedly ignored settings.
| Parameter | Type | wan2.6/2.7-t2v | wan2.7-i2v | wan2.7-r2v | wan3.0-video | minimax/h3 | minimax/h3-fast |
|---|---|---|---|---|---|---|---|
model | string | ✅ Required | ✅ | ✅ | ✅ | ✅ | ✅ |
prompt | string | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
size | string | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ Only 1344x768 / 768x1344, defaulting to the former |
seconds | int | ✅ | ✅ | ✅ | ✅ | ✅ Integer 4~15 | ✅ Integer 5~15, default 5 (15 generates and bills at 14 seconds) |
input_reference | string / file | ❌ | ✅ URL / base64 only | ❌ | ✅ URL / base64 only | ✅ Also accepts file uploads | ✅ Also accepts file uploads |
media | array | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
prompt_extend | bool | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
watermark | bool | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
seed | int | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ 0~2147483647 |
audio | bool | ❌ | ❌ | ❌ | ✅ | ❌ (always has audio) | ❌ (always has audio) |
Details:
prompt_extend: enabled by default and rewritesprompt. Setfalseexplicitly to preserve your prompt.watermark: defaults tofalse; controls an AI-generated watermark.seed: random seed for reproducing results.minimax/h3does not support it and can produce different results for the same input.minimax/h3-fastsupports it: identical inputs and seeds generally preserve composition, without guaranteeing pixel-identical output. Pass it throughextra_body, likemedia:extra_body={"seed": 42}.audio: whether to generate audio; the unit price is the same either way.
Boolean parameters must be actual booleans; the string "true" returns 400. seconds and seed must likewise be convertible to integers.
Unrecognized fields are never forwarded upstream. The gateway builds the upstream request field by field, accepting only this table’s parameters. All other fields, however supplied, are discarded and affect neither generation nor billing.
Thus using extra_body for media is necessary and safe, but it does not enable parameters outside this table. Also, an explicit top-level extra_body field in raw HTTP returns 400; that path has different semantics, explained in the error.
media types
media is an array of {"type": "…", "url": "…"}. Alongside public URLs, url accepts inline data:<mime>;base64,... for images, audio, and video, avoiding a separate hosting service. Inline data increases body size by about 1/3; keep each asset within a few MB where possible.
The model’s compute node downloads URLs directly; the gateway does not relay or store the files. A URL must:
- Be a publicly accessible
http:///https://address, with no credentials and no internal-network destination; otherwise submission returns400. - Remain valid when the task executes. Allow sufficient expiry for signed links, including queue time.
- Respond quickly and serve a reasonably sized asset.
Prefer addresses reliably reachable from mainland China, such as local object storage or CDNs. Overseas sites often time out from the compute nodes.
Download failures fail the task without a charge. minimax/h3-fast identifies the failing asset, for example media[0]: media link returned HTTP 404. The same rules apply to URL-based input_reference.
type | Purpose | wan3.0-video | minimax/h3 |
|---|---|---|---|
first_frame | First image frame, equivalent to input_reference | ✅ | ✅ |
last_frame | Last image frame | ✅ | ✅ |
reference_image | Image guiding subject / style | ✅ | ✅ |
reference_video | Video reference | ✅ | ✅ |
reference_audio | Audio guiding voice / style | ✅ | ✅ |
file | File ≤ 100 MB | ✅ | ❌ |
link | Webpage link | ✅ | ❌ |
first_clip | Starting clip | ❌ | ❌ |
driving_audio | Audio for lip / motion alignment | ❌ | ❌ |
This table covers only wan3.0-video and minimax/h3, whose available types have been confirmed. The gateway does not narrow media types for other Wan models such as wan2.7-i2v / wan2.7-r2v; the upstream decides. Check the model page and handle failures as follows.
Wan can accept task creation and then fail with InvalidParameter after roughly ten seconds for a type the model does not accept. Failed tasks are not charged; do not depend on gateway rejection.
H3 returns 400 on submission, listing its supported types.
Billing
Videos are charged by the model's billable duration (¥/second). Wan3.0 reference video input is also billed, as described below. See actual prices in the catalog.
Unit price varies by resolution tier, selected by the short side of size. Both 1920x1080 and 1080x1920 use 1080P. Estimate costs using the short side and the model’s tier price.
For wan3.0-video and wan3.0-video-prime, both reference video input and generated output are billed. Both use the same rate selected by output resolution, combined once. For example, 10 seconds of reference video + 10 seconds of output = 20 billable seconds. Without reference video input, only generated output is billed. seconds sets the generated output duration, excluding reference video duration.
Request logs show reference video input, generated video output and the billed duration separately. Input and output show recorded durations; the charge breakdown gives the billed quantity and amount. Do not add input duration again to a total that already includes it.
On completion, usage includes duration details. duration_seconds is the gateway's billing basis. This example has no reference video input:
{
"duration_seconds": 5, // Billed seconds; includes reference video input for Wan3.0
"SR": 480, // Actual output resolution tier; determines the unit price
"ratio": "16:9",
"fps": 30,
"video_count": 1
}The actual output tier governs billing: settlement reads usage.SR, not the requested size. With image-to-video, the upstream may adjust aspect ratio to the reference image; target pixel count still determines the tier.
Price is independent of canvas: all six cost the same per second.
Billed seconds equal requested seconds, rather than the slightly longer output duration described above. The extra duration is free.
{
"duration_seconds": 4, // = requested seconds; billed on this value
"size": "768x768",
"inference_time_s": 54.8 // Generation time for reference only; not used for billing
}Price is independent of canvas: both cost the same per second.
Billed seconds equal requested seconds, with no charge for the slightly longer output. seconds=15 is billed as 14 seconds because the model’s maximum output is about 14.38 seconds.
usage.duration_seconds is authoritative; tasks requesting seconds=15 return 14 here:
{
"duration_seconds": 5, // = requested seconds (15 returns 14); billed on this value
"size": "1344x768",
"inference_time_s": 17.9 // Generation time for reference only; not used for billing
}General rules:
- Failed generation is not charged.
- Polling has no additional charge. A task settles once when its completed status is first read; later queries do not charge again.
- Downloads are not billed separately.
Check individual charges in request logs.