GuidesBatch-Generate 15-Second Product Videos from Photos via API
Makernice Team · ·
Prices as of November 2, 2026 (price list v12).
To turn a folder of product photos into 15-second videos with one Makernice API key, quote each clip, create it with an Idempotency-Key, then poll or take a webhook. In image-to-video mode at 720p, wan-3.0 costs $1.18 per 15-second clip and $11.84 for 10 clips (price list v12, November 2, 2026), and the quote shows the exact amount before anything is held.
What do you need before you batch-generate product videos?
- An API key from the console with a spend limit and an expiry date for this batch. A create request that would go over the limit gets 402 key_spend_limit_reached before any money is held.
- A public https URL for each product photo: a still JPEG, PNG or WebP of at most 10 MB, served directly at that URL. Redirects are not followed.
- One prompt per product that describes the motion, such as a slow turn on a tabletop or a push-in on the label.
wan-3.0accepts prompts of up to 4,000 characters. - A balance that covers the batch. Each top-up is $15 to $1,000 and the balance never expires.
How much does a batch of 15-second product videos cost?
A video costs its unit price times the seconds you request, so a 15-second clip is 15 times the per-second price and 10 clips are 150 seconds. The table uses price list v12 (November 2, 2026); the cost calculator gives the same numbers for 15 seconds, 10 clips and a 0% regeneration rate. Open this batch in the calculator →
| `model` | Resolution | Per second | 15-second clip | 10 clips |
|---|---|---|---|---|
wan-3.0 | 480p | $0.0394 | $0.59 | $5.92 |
wan-3.0 | 720p | $0.0789 | $1.18 | $11.84 |
seedance-2.5 | 720p | $0.2592 | $3.89 | $38.88 |
Use seedance-2.5 when you need to set the shape, such as 9:16 for a vertical feed: only the Seedance models accept aspect_ratio in image-to-video, and the other models reject the field with 400 invalid_request. Prices change only when a new price list is published, and the quote is always the amount you pay.
Step 1: How do you quote one product clip?
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "wan-3.0",
"mode": "i2v",
"prompt": "The ceramic mug turns slowly on a white tabletop, soft studio light, steam rising, slow push-in",
"image_url": "https://example.com/products/sku-1042.jpg",
"duration": 15,
"resolution": "720p"
}'The response has amount in micro-USD (millionths of a US dollar): the unit price times facts.quantity, which is 15 here. The quote is valid for 12 minutes and for one generation, and quoting holds and charges nothing.
Step 2: How do you create a clip without paying twice?
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: product-video-sku-1042-v1" \
-H "Content-Type: application/json" \
-d '{
"quote_id": "qt_...",
"input": {
"model": "wan-3.0",
"mode": "i2v",
"prompt": "The ceramic mug turns slowly on a white tabletop, soft studio light, steam rising, slow push-in",
"image_url": "https://example.com/products/sku-1042.jpg",
"duration": 15,
"resolution": "720p"
}
}'Send the same input with the quote_id. Use one Idempotency-Key per product, built from the SKU: if your script retries after a timeout, the same key, quote and input return the original generation with 200 and nothing is charged again. A key is 1 to 200 printable ASCII characters with no spaces.
Step 3: How do you run the whole batch?
import csv
import os
import time
from decimal import Decimal
import requests
API = "https://api.makernice.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MAKERNICE_API_KEY']}"}
FINAL = {"succeeded", "failed", "canceled"}
def call(method, path, headers=None, **kwargs):
# Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; raises on anything else
for _ in range(20):
r = requests.request(method, API + path, headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
if not r.ok:
raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text}")
return r.json()
raise RuntimeError(f"{method} {path}: still rate limited")
# products.csv columns: sku,image_url,prompt
with open("products.csv", newline="") as f:
products = list(csv.DictReader(f))
for p in products:
job = {
"model": "wan-3.0",
"mode": "i2v",
"prompt": p["prompt"],
"image_url": p["image_url"],
"duration": 15,
"resolution": "720p",
}
# Quote right before creating: a quote is valid for 12 minutes and one generation
quote = call("POST", "/generations/quote", json=job)
print(p["sku"], "quoted", Decimal(quote["amount"]).scaleb(-6), "USD")
gen = call(
"POST",
"/generations",
json={"quote_id": quote["id"], "input": job},
headers={"Idempotency-Key": f"product-video-{p['sku']}-v1"},
)
while gen["status"] not in FINAL:
time.sleep(5)
gen = call("GET", f"/generations/{gen['id']}")
print(p["sku"], gen["status"], gen["billing"], gen.get("error"))The script quotes each product right before it creates it and waits out 429 responses for the seconds in Retry-After. It runs one clip at a time; an account has 2 generations in progress by default, so a second worker is the most that helps until support raises the limit. To skip polling, pass a callback_url when you create each clip and we POST the finished generation to your server.

Which errors show up in product-video batches?
| What you see | Why | What to do |
|---|---|---|
failed with invalid_request, error.input: image_url | Redirect, over 10 MB, GIF or animated WebP | Fix the photo URL and create again; the hold was released |
400 invalid_request on the quote | A field the mode doesn't take, such as aspect_ratio | Remove the field or use a Seedance model |
410 quote_expired | More than 12 minutes between quote and create | Quote again right before you create |
429 concurrency_limit_reached | 2 generations are already in progress | Wait the Retry-After seconds and retry |
402 daily_spend_limit_reached | The account hit its daily spend limit | Resume after 00:00 UTC or ask support |
422 idempotency_conflict | A key was reused with a different input | Use a new key for the new input |
What happens to the money when a product clip fails?
Creating a clip holds its quoted amount. If the clip fails or times out, the hold is released in full; a released hold is not a refund, because the money never left your balance. Note: if a finished output is blocked by output moderation for a policy violation, the run is still charged. You can contact support for a manual review. Every case is listed under Are failed generations charged?
Next, read API best practices for retries and polling, or image-to-video fields for what each model accepts.
Frequently asked questions
Can I make a 9:16 product video from a photo through the Makernice API?
aspect_ratio values including 9:16 in image-to-video. The other models reject the field in that mode with 400 invalid_request.How much does a 15-second image-to-video clip cost on Makernice?
wan-3.0 at 720p is $1.18 and seedance-2.5 at 720p is $3.89; the quote you get before each clip is the amount you pay.Am I charged for product clips that fail?
Models in this post
VideoWan 3.0
-21%The widest duration range on Makernice — 2 to 30 seconds — at 480p, 720p or 1080p, with every input mode and up to 10 references.
VideoSeedance 2.5
ByteDance's flagship for long takes: up to 30 seconds, 480p to 4K, and up to 16 reference images to keep every shot on-model. Audio is always on (no toggle).