Skip to main content

AI Image and Video API Starter Guide: From First Input to Reviewed Output

Make a blue-mug image and animate it with MiniMax H3. Start in the Playground, then use Python, CLI or an agent, with prompts, costs and output checks.

Author
Lydia
Published
Read
12 min read
Updated
A fictional adult ceramic artist in a blue apron places a blue cup beside a camera in a pottery workshop

Make one image, approve it, then animate that image into a short clip. These steps follow our October 2026 blue-mug test. This guide uses a $0.010 Z-Image still and five seconds of MiniMax H3 at 768p for $0.185: $0.195 for one attempt at each stage, with prompt expansion off for the still. Try the same blue-mug scene in the Playground first; once its inputs work, use the Python example or let an agent handle the calls.

The result you want is specific: a well-shaped mug on a workbench, steam drifting right, and a slow camera push that ends in a hold. You do not need to deploy a model or set up a GPU server.

Try the complete case in the Playground

Create and fund your account, and verify your email. The Playground handles the request; an API key is only needed for the code and agent tools below.

1. Generate the opening image

Open Z-Image text to image and use its Playground. Paste this prompt:

Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench in a pottery studio, a thin curl of steam rising from the mug. Soft window light from the left, shallow depth of field, shelves of pots softly out of focus behind. The mug sits left of centre with empty bench to its right. Natural colours, no text, no logos.

Choose 16:9, PNG, seed 42, and turn prompt expansion off. Check the displayed price, then press Run model. The baseline image costs $0.010; expansion would raise it to $0.016. Keep seed 42 fixed while comparing changes to the prompt.

Open the result before continuing. Check:

  • Object: one mug, one handle, and clean ceramic geometry.
  • Movement opportunity: visible steam and room to the right for it to drift.
  • Composition: the mug sits left of centre; the shelves remain a quiet background.
  • Unwanted details: no stray lettering, logos or additional cups.

Download the accepted PNG as image.png. If the mug is wrong, change one sentence and make a new image before spending on video. Each new baseline image adds $0.010.

2. Animate that approved image

Open MiniMax H3 image to video, add image.png as the Input image, and paste into Prompt:

Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.

Set Duration to 5 seconds and Resolution to 768p, check the price and press Run model. This clip costs $0.185; choosing 2k would cost $0.290.

The prompt separates steam movement, camera movement and sound. It asks the mug to stay in place rather than describing it again. If the mug walks across the bench, simplify the camera instruction rather than adding more object actions. A slow push-in moves the camera closer; it is not a request for the mug to move.

3. Review the clip, then choose the next take

Play the whole result with sound. Check that the mug does not slide or change shape, the camera pushes in then holds, the clip has no unwanted cut, and the audio fits a quiet studio. A roughly five-second MP4 with an audio stream is the expected deliverable; having a playable file alone does not make the shot usable.

If the picture was right but the motion was wrong, keep the accepted opening image and revise the motion prompt. If the opening picture was wrong, replace that image first. H3 has no seed: generating at 2k produces a new take, which can move differently from the 768p version you liked.

What the blue-mug run produced

The still cost $0.010 and arrived as a 1280 × 720 PNG. The five-second video request cost $0.125 (at 480p, which is no longer offered; the same clip at 768p costs $0.185) and returned a 832 × 480 MP4 with an audio track. The total was $0.135; at 768p it would be $0.195.

The mug stayed on the bench while steam moved upward and right; the camera push was subtle. The still placed the mug only slightly left of centre, and the steam softened near the shelf toward the end. For a tighter product shot, make the mug's placement more explicit in the still prompt before animating it.

Why these two models fit the first run

Z-Image gives you an inexpensive frame to settle the subject and composition. H3 accepts that image and generates both motion and a soundtrack. Their inputs differ:

Z-Image text to imageH3 image to video
Model IDalibaba/z-image-turbo/text-to-imageminimax/h3/image-to-video
Required inputprompt (1 to 1,000 characters)prompt (up to 7,000) and image_url
Settings used hereaspect_ratio (1:1, 4:3, 3:4, 16:9 or 9:16), output_format (only png), seed, enable_prompt_expansionduration_seconds (4 to 15), resolution (768p or 2k)
Not acceptedany other output formatseed and aspect_ratio: the opening frame sets the shape
Returnsone PNGone MP4 with a generated soundtrack
Price$0.010 per image$0.037 per second at 768p

Use a PNG, JPEG, WebP or GIF opening image, each side between 256 and 5,760 pixels, with an aspect ratio between 2:5 and 5:2 and a file under 30 MB. An uploaded PNG is the simplest input. If you use a public image link instead, it must return the actual image; a cloud-drive sharing page is not an image input.

Run the same case from Python

Use Python 3.11 or later, install the official package with python -m pip install spicyapi, and have the file utility available. Set SPICY_API_KEY in your own terminal, save this as blue-mug.py, and run python blue-mug.py. It asks for approval at both paid stages and pauses for your image review. Keep the generated run folder when resuming; choose a new RUN_NAME only for a deliberately new attempt. For the API contract and SDK options, use the Python SDK documentation.

python
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
import urllib.request
import uuid
from spicyapi import SpicyClient, output_assets

# Change RUN_NAME for a deliberately new pair of paid generations.
folder = Path(os.getenv("RUN_NAME", "blue-mug-1"))
folder.mkdir(exist_ok=True, mode=0o700)
state_file = folder / "state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {}
identity = hashlib.sha256(os.environ["SPICY_API_KEY"].encode()).hexdigest()
if state.get("identity", identity) != identity:
    raise RuntimeError("This saved run belongs to a different API key.")
state["identity"] = identity
client = SpicyClient()

def save() -> None:
    temp = state_file.with_suffix(".tmp")
    temp.write_text(json.dumps(state, indent=2))
    temp.chmod(0o600)
    temp.replace(state_file)

def run(stage: str, model: str, inputs: dict) -> dict:
    request = {"model": model, "input": inputs}
    if stage not in state:
        state[stage] = {"request": request, "key": str(uuid.uuid4())}
    saved = state[stage]
    if saved["request"] != request:
        raise RuntimeError("Input changed: choose a new RUN_NAME, not a new retry key.")
    save()  # The same request and key survive a lost create response.
    if not saved.get("taskId"):
        if "first_sent" in saved and time.time() - saved["first_sent"] >= 23 * 3600:
            raise RuntimeError("Unknown acceptance: reconcile task history before retrying.")
        if "quote" not in saved:
            quote = client.quote_task(model=model, input_data=inputs)
            if input(f"{stage}: quoted ${quote['estimatedCost']}. Create? [y/N] ") != "y":
                raise SystemExit("No task created for this stage.")
            saved["quote"] = quote
        saved.setdefault("first_sent", time.time())
        save()
        quote = saved["quote"]
        task = client.create_task(model=model, input_data=inputs,
                                  idempotency_key=saved["key"],
                                  quote_id=quote["quoteId"],
                                  expected_cost=quote["estimatedCost"])
        saved["taskId"] = task["taskId"]
        save()
    task_id = saved["taskId"]
    print(stage, "task:", task_id)
    deadline = time.monotonic() + 1200
    while time.monotonic() < deadline:
        task = client.get_task(task_id)
        if task["state"] in {"failed", "canceled", "expired"}:
            raise RuntimeError(f"{task_id}: {task.get('errorCode')} {task.get('errorMessage')}")
        if task["state"] == "succeeded":
            assets = output_assets(task)
            if not assets or assets[0].get("unavailable"):
                raise RuntimeError(f"{task_id}: no retrievable output; keep the task ID.")
            asset = assets[0]
            if not asset.get("pending") and asset.get("url"):
                print("Charge:", task.get("cost"), "settled:", task.get("settled"))
                return asset
        time.sleep(5)
    raise TimeoutError(f"Resume {task_id} by rerunning this file; do not start another task.")

def download(asset: dict, destination: Path, mime: str) -> None:
    temporary = None
    try:
        request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
        with urllib.request.urlopen(request, timeout=300) as response:
            if response.status != 200 or response.headers.get_content_type() != mime:
                raise RuntimeError("Unexpected download status or content type.")
            with tempfile.NamedTemporaryFile(dir=folder, delete=False) as handle:
                temporary = Path(handle.name)
                while chunk := response.read(1024 * 1024):
                    handle.write(chunk)
        size = temporary.stat().st_size
        expected = int(asset.get("bytes") or 0)
        detected = subprocess.check_output(["file", "-b", "--mime-type", str(temporary)], text=True).strip()
        if not size or (expected and size != expected) or detected != mime:
            raise RuntimeError("Incomplete or incorrectly formatted download.")
        temporary.replace(destination)  # An interrupted transfer leaves the old file alone.
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

image_input = {
    "prompt": (
        'Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench '
        'in a pottery studio, a thin curl of steam rising from the mug. Soft window '
        'light from the left, shallow depth of field, shelves of pots softly out of '
        'focus behind. The mug sits left of centre with empty bench to its right. '
        'Natural colours, no text, no logos.'
    ),
    "aspect_ratio": "16:9",
    "output_format": "png",
    "seed": 42,
    "enable_prompt_expansion": False,
}
image_asset = run("image", "alibaba/z-image-turbo/text-to-image", image_input)
download(image_asset, folder / "image.png", "image/png")
if input(f"Open {folder / 'image.png'}. Is the mug/frame approved? [y/N] ") != "y":
    raise SystemExit("Stopped before video generation.")
if not state.get("image_uri"):
    state["image_uri"] = client.upload_file(str(folder / "image.png"))["uri"]
    save()
video_input = {"image_url": state["image_uri"], "prompt": 'Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.',
               "duration_seconds": 5, "resolution": "768p"}
video_asset = run("video", "minimax/h3/image-to-video", video_input)
download(video_asset, folder / "video.mp4", "video/mp4")
print("Play", folder / "video.mp4", "with sound and review it before using it.")

Keep the API key on your server or a machine you control, never in a public web page or mobile app. The model pages above remain the place to check current availability and accepted inputs before integrating a different model.

Let an agent call it when that is easier

Use an Agent when you want it to prepare the inputs and return the result while you approve the price and creative choices. Review the opening image yourself before authorising the video.

  • CLI: with the same input object saved as input.json, use npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. The CLI confirms the price; keep the returned task ID for recovery. For the clip, upload the approved still with npx @spicyapi/cli files upload image.png, then create minimax/h3/image-to-video the same way with the returned spicy:// URI as image_url.
  • MCP: ask your assistant to use spicyapi_task_create for alibaba/z-image-turbo/text-to-image, then spicyapi_task_wait; for the clip, add spicyapi_upload_file for your approved image.png, then spicyapi_task_create with minimax/h3/image-to-video, 5 seconds, 768p and the motion prompt above. The MCP server requests price confirmation before creation.
  • Skill: install with npx skills add spicyapi-ai/spicy-skill, then ask $spicyapi to run the same workflow and stop for your approval. The Skill uses a configured CLI or MCP connection.

Budget for attempts, not just the final clip

These estimates are in US dollars. Confirm the live price before generating.

RequestEstimated price
One Z-Image still, 16:9 PNG$0.010
Same still with prompt expansion on$0.016
H3, 4 s at 768p$0.148
H3, 5 s at 768p (the default if resolution is omitted)$0.185
H3, 5 s at 2k$0.290
H3, 10 s at 768p$0.370
H3, 15 s at 2k$0.870
  • Length and resolution: H3 costs $0.037 or $0.058 per second at 768p or 2k.
  • Unsupported combinations: a 4-second clip at 2k is refused with error 40004.
  • Two drafts: one still plus two five-second 768p videos costs $0.380.
  • Draft then delivery take: one still, a five-second 768p check and one five-second 2k take costs $0.010 + $0.185 + $0.290 = $0.485, reusing the accepted still.

Money is held when a task is accepted and settled when it ends. Failed and expired tasks normally release the hold; certain content refusals are charged under the model's stated policy. Check the task's actual cost and settled status when investigating a charge.

Fix the problem at the right stage

SymptomWhat to change
The mug has two handles or stray textRevise the image prompt and approve a new opening image before generating video
The mug moves when you wanted the camera to moveKeep the image; ask for one camera move and explicitly keep the mug stationary
The video looks good at 768p but the new 2k take differsTreat it as a new shot and check the movement before accepting it
The image link failsUpload the image file, or use a direct, reachable image URL rather than a sharing page
The account returns 401 or 40310Check the key, or complete email verification
40301 / 40302 / 40303Check model permissions, the key's IP allowlist or regional availability
40201 / 40202Check available balance or the account/key spending cap; keep the saved request when reconciling a failed submission
A request rejects seed, aspect_ratio or output_formatCheck the selected task's supported inputs; do not copy Z-Image fields into H3
40004Change the incompatible combination: 4 seconds at 2k
40901 or an idempotency conflictRequote: if no task ID was saved, delete that stage's entry in state.json and rerun. If the earlier attempt may have been accepted, check your task list before creating anything else; see the error reference
429 or a connection errorKeep the run folder and task ID; do not start another paid attempt just to recover the result
A task reports content_rejected or timeoutRead its error and charge, then deliberately choose a changed prompt, source image or shorter shot
Generation succeeded but the result is not readyResume the same saved task; do not generate another clip to obtain a download

Keep the first workflow small

Once the shot works, decide its delivery length and resolution, then generate and review that take. An optional closing image can guide the ending through H3's last_image_url; it does not guarantee an exact last frame or the motion between frames.

For an application, keep the two task IDs and store the accepted files in your own storage. Use polling versus webhooks to choose result collection, media storage to keep files, and batch generation when you need many shots. The first milestone remains the same: one approved image and one clip that actually follows your intended shot.

Also in 日本語 · 한국어 · Deutsch · Français · Español · Português (Brasil) · Русский · Italiano · Polski

Try it yourself

Pay only for what you run — no plan, no subscription.

Made with MiniMax H3 · ByteDance Seedance 2.0 Fast