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 image | H3 image to video | |
|---|---|---|
| Model ID | alibaba/z-image-turbo/text-to-image | minimax/h3/image-to-video |
| Required input | prompt (1 to 1,000 characters) | prompt (up to 7,000) and image_url |
| Settings used here | aspect_ratio (1:1, 4:3, 3:4, 16:9 or 9:16), output_format (only png), seed, enable_prompt_expansion | duration_seconds (4 to 15), resolution (768p or 2k) |
| Not accepted | any other output format | seed and aspect_ratio: the opening frame sets the shape |
| Returns | one PNG | one 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.
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, usenpx @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 withnpx @spicyapi/cli files upload image.png, then createminimax/h3/image-to-videothe same way with the returnedspicy://URI asimage_url. - MCP: ask your assistant to use
spicyapi_task_createforalibaba/z-image-turbo/text-to-image, thenspicyapi_task_wait; for the clip, addspicyapi_upload_filefor your approvedimage.png, thenspicyapi_task_createwithminimax/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$spicyapito 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.
| Request | Estimated 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
| Symptom | What to change |
|---|---|
| The mug has two handles or stray text | Revise the image prompt and approve a new opening image before generating video |
| The mug moves when you wanted the camera to move | Keep the image; ask for one camera move and explicitly keep the mug stationary |
| The video looks good at 768p but the new 2k take differs | Treat it as a new shot and check the movement before accepting it |
| The image link fails | Upload the image file, or use a direct, reachable image URL rather than a sharing page |
The account returns 401 or 40310 | Check the key, or complete email verification |
40301 / 40302 / 40303 | Check model permissions, the key's IP allowlist or regional availability |
40201 / 40202 | Check 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_format | Check the selected task's supported inputs; do not copy Z-Image fields into H3 |
40004 | Change the incompatible combination: 4 seconds at 2k |
40901 or an idempotency conflict | Requote: 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 error | Keep the run folder and task ID; do not start another paid attempt just to recover the result |
A task reports content_rejected or timeout | Read its error and charge, then deliberately choose a changed prompt, source image or shorter shot |
| Generation succeeded but the result is not ready | Resume 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.







