GoodPut AI

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.

OperationEndpoint
SubmitPOST /v1/videos
Query status and video download URLGET /v1/videos/{video_id}
DownloadGET /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.mp4

Get 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
}
FieldMeaning
video_urlDirect link for a completed task; null while unfinished, on failure, or when the provider has no direct link
video_url_expires_atLink expiry as a Unix timestamp in seconds; null when the expiry cannot be determined
expires_atTask 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.

ModeTriggerTypical use
Text to videoprompt onlyGenerate from scratch
Image to videoinput_reference or media.first_frameAnimate an image
Reference to videomedia.reference_image / reference_video / reference_audioPreserve a subject, style, or voice
First/last framesmedia.first_frame + media.last_frameSpecify 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-t2v is text-only, wan2.7-i2v image-only, and wan2.7-r2v reference-only. Choosing the wrong model returns 400. wan3.0-video supports all four modes in one model.
  • minimax/h3 handles all four modes in one model, selecting from the inputs automatically.
  • minimax/h3-fast uses the same calling convention as minimax/h3, but supports only 1344x768 / 768x1344, seconds from 5~15, and seed. 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:

StatusMeaningAction
queuedWaiting to startKeep polling
in_progressGeneratingKeep polling
completedFinishedDownload or play video_url; fall back to /content if no direct URL is available
failedGeneration failedRead 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 sideTier
480480P
540540P
720720P
10801080P

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:

Ratio480P540P720P1080P
16:9 landscape832x480960x5401280x7201920x1080
9:16 portrait480x832540x960720x12801080x1920
1:1 square480x480540x540720x7201080x1080
4:3640x480720x540960x7201440x1080
3:4480x640540x720720x9601080x1440

1280x720, 1280*720, and 1280×720 are all accepted.

Unsupported combinations fail rather than being silently replaced.

InputResult
1024x1024, 800x600: short side is not a tier number400, listing valid short sides
2560x1080: matches 1080P short side but uses unsupported 21:9400, listing supported ratios
1920x1080 for a model supporting only 480P400 (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:

Ratiosize
21:91536x672
16:9 landscape1344x768
4:31024x768
1:1 square768x768
3:4768x1024
9:16 portrait768x1344

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:

Ratiosize
16:9 landscape1344x768 (default)
9:16 portrait768x1344

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.

ParameterTypewan2.6/2.7-t2vwan2.7-i2vwan2.7-r2vwan3.0-videominimax/h3minimax/h3-fast
modelstring✅ Required✅✅✅✅✅
promptstring✅✅✅✅✅✅
sizestring✅✅✅✅✅✅ Only 1344x768 / 768x1344, defaulting to the former
secondsint✅✅✅✅✅ Integer 4~15✅ Integer 5~15, default 5 (15 generates and bills at 14 seconds)
input_referencestring / file❌✅ URL / base64 only❌✅ URL / base64 only✅ Also accepts file uploads✅ Also accepts file uploads
mediaarray❌✅✅✅✅✅
prompt_extendbool✅✅✅✅❌❌
watermarkbool✅✅✅✅❌❌
seedint✅✅✅✅❌✅ 0~2147483647
audiobool❌❌❌✅❌ (always has audio)❌ (always has audio)

Details:

  • prompt_extend: enabled by default and rewrites prompt. Set false explicitly to preserve your prompt.
  • watermark: defaults to false; controls an AI-generated watermark.
  • seed: random seed for reproducing results. minimax/h3 does not support it and can produce different results for the same input. minimax/h3-fast supports it: identical inputs and seeds generally preserve composition, without guaranteeing pixel-identical output. Pass it through extra_body, like media: 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 returns 400.
  • 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.

typePurposewan3.0-videominimax/h3
first_frameFirst image frame, equivalent to input_reference✅✅
last_frameLast image frame✅✅
reference_imageImage guiding subject / style✅✅
reference_videoVideo reference✅✅
reference_audioAudio guiding voice / style✅✅
fileFile ≤ 100 MB✅❌
linkWebpage link✅❌
first_clipStarting clip❌❌
driving_audioAudio 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.

On this page