Image-to-video on the PixVerse API is not one request. The still goes up first, the response hands back an integer, and only then does the generation call exist. That integer, img_id, is the join between the two halves, and every failure mode on the upload side resurfaces as a generation error.

The image-to-video endpoint

The overview page states the shape in its first paragraph: The Image-to-Video API transforms static images into dynamic videos with motion and effects. The endpoint it names is POST https://app-api.pixverse.ai/openapi/v2/video/img/generate.

Two calls, one billable one

The upload call produces no video. It moves bytes to the vendor and returns a reference. The generation call is the one that consumes credits, and the one that appears in the concurrency ceiling, because it is the call that creates a task in generating state. The API guide covers the auth headers both calls need.

Why the still becomes an id

The generation endpoint accepts JSON and validates img_id as an integer <uint64>, so a base64 blob or a local path has nowhere to go. The upload endpoint turns a file into that integer, and it accepts either bytes or a URL, so a still already on public storage passes through the same handshake.

Upload the still for an img_id

# Step 1: POST the still as multipart/form-data and keep Resp.img_id.
curl --location --request POST 'https://app-api.pixverse.ai/openapi/v2/image/upload' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --form 'image=@"storyboard.png"' \
  > /tmp/pixverse-upload.json

jq -r '.ErrCode, .Resp.img_id, .Resp.img_url' /tmp/pixverse-upload.json

A file or a URL

The upload body takes two optional fields, image and image_url, and the reference documents a constraint on both: Either image or image_url is required. The URL variant is picky in a way worth knowing before you build against it — application/octet-stream is not supported, so the server has to serve a recognisable image content type.

What the upload validates

The upload reference lists three rules: maximum dimensions 10000 pixels, file size less than 20MB, and Supported formats: "png", "webp", "jpeg", "jpg" with supported mime-type "image/jpeg","image/jpg","image/png","image/webp". Pixel dimensions matter independently of file size, so a heavily compressed 12000-pixel scan fails even at four megabytes.

The img_id chain, end to end

Four-step sequence diagram of the PixVerse image-to-video chain: a local still is posted to the upload endpoint, the response returns img_id and img_url, the generation call posts that img_id, and the status endpoint is polled until status 1 returns a video url

Submit, hold the video_id, poll

Each hop carries one value forward. img_id goes from the upload response into the generation body; video_id goes from the generation response into the status URL.

# The whole chain, with every documented field named.
img_id=$(curl -s --request POST \
  'https://app-api.pixverse.ai/openapi/v2/image/upload' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --form 'image=@"storyboard.png"' | jq -r '.Resp.img_id')

video_id=$(curl -s --request POST \
  'https://app-api.pixverse.ai/openapi/v2/video/img/generate' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --header 'Content-Type: application/json' \
  --data-raw "{
    \"img_id\": ${img_id},
    \"model\": \"v6\",
    \"prompt\": \"the camera pushes in slowly, warm light across the rim\",
    \"duration\": 5,
    \"quality\": \"720p\",
    \"motion_mode\": \"normal\",
    \"seed\": 0
  }" | jq -r '.Resp.video_id')

echo "img ${img_id} produced video ${video_id}"

The generation call and its fields

Five body parameters are marked required: duration, img_id, model, prompt and quality. The rest are optional, and the reference marks which model generations each one supports.

# Step 2: the generation call, with optional fields commented out.
curl --location --request POST 'https://app-api.pixverse.ai/openapi/v2/video/img/generate' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --header 'Content-Type: application/json' \
  --data '{
    "img_id": 98765,
    "model": "v6",
    "prompt": "the camera pushes in slowly, warm light across the rim",
    "duration": 5,
    "quality": "720p",
    "motion_mode": "normal",
    "negative_prompt": "text overlay, warped faces",
    "seed": 0,
    "generate_audio_switch": false
  }'

Duration depends on the model

duration is an integer whose legal values change with the model, and the reference spells out the bands: v.3.5/v4/v4.5 : 5/8 (v3.5 1080p cannot use 8), v5 : 5/8, v5.5/v5.6 : 5/8/10 (1080p cannot use 10) and v6/c1 : 1~15. A request that hardcodes eight seconds works on several generations and fails on the newest ones, where any value from one to fifteen is accepted.

Switches, seeds and motion

generate_audio_switch is documented as Supported in v5.5/5.6/v6/c1 models with default value : false, and generate_multi_clip_switch as Supported in v5.5/v6 model, also defaulting to false. seed runs 0 - 2147483647. motion_mode and negative_prompt appear in the vendor’s own curl example but not in the parameter table, and motion_mode is documented elsewhere as defaulting to normal, with fast limited to five-second clips and unsupported at 1080p.

Polling until status 1

# Step 3: poll the documented status endpoint until the task leaves status 5.
while :; do
  response=$(curl -s \
    "https://app-api.pixverse.ai/openapi/v2/video/result/${video_id}" \
    --header "API-KEY: ${PIXVERSE_API_KEY}" \
    --header "Ai-trace-id: $(uuidgen)")
  status=$(jq -r '.Resp.status' <<<"$response")
  case "$status" in
    1) jq -r '.Resp.url' <<<"$response"; break ;;
    5) sleep 4 ;;
    7) echo 'moderation failure, credits refunded' >&2; break ;;
    8) echo 'generation failure' >&2; break ;;
    *) echo "unexpected status $status" >&2; break ;;
  esac
done

Status values

The endpoint page annotates its sample response with 1: Generation successful, 5: In progress, 7: Moderation failed, 8: Generation failed. The vendor index adds a sixth, 6 = deleted, which the endpoint page omits, so a client built from the endpoint page alone treats it as unknown. Status 7 is refunded, costing nothing but the wait.

Failures from the upload hop

The image codes

400032 Invalid image ID is the one to expect when a path, a filename or a stale integer reaches the generation body in place of a real img_id. 500030 Image size cannot exceed 20M , 10000px, 500031, 500032, 500033, 500041 and 500042 all belong to the upload hop, and several of them are documented only in the error table rather than on the upload page, which is why the upload page is not enough on its own.

# Read the code, then decide whether the fix is upstream.
code=$(jq -r '.ErrCode' /tmp/pixverse-upload.json)
case "$code" in
  0)      echo 'upload accepted, take Resp.img_id' ;;
  500030) echo 'dimensions or weight over the limit' ;;
  500032) echo 'format refused, check the mime type' ;;
  500041) echo 'transfer failed, retry the upload' ;;
  *)      echo "unexpected upload error $code" ;;
esac

A cap the docs disagree on

The generation reference allows Prompt <= 5000 characters. The troubleshooting page states =< 2048 characters for the same two fields, and codes 400018 and 400019 both read Prompt/negative prompt length exceeds 2048 characters limit. Two published sources say 2048 and one says 5000, so stay under the smaller number until the vendor reconciles them. The rate-limit page covers the concurrency ceiling a successful submit then meets.

Frequently asked questions

Does the generation body take an image URL? No. It takes img_id, documented as Image ID from Upload image API. A URL goes to the upload endpoint’s image_url field, and the id comes back from there.

How long does an img_id stay valid? Not published by the vendor. Nothing on the upload or generation pages states an expiry, so mint one per job rather than caching ids.

Which fields are required? duration, img_id, model, prompt and quality. Everything else, including seed and the audio switches, is optional.

Can I reuse one img_id for several generations? The field accepts a single id and the reference mentions single image or single-image templates. Whether the same id can be submitted twice is not published, and multi-image effects take img_ids instead.

Why does a five-second clip cost more at 1080p? Because billing counts seconds at a rate that depends on quality and audio. The published rate card has the per-second numbers.