Makernice
← All posts
A row of product boxes on a studio table, each lit by a small lamp, with a camera slider in frontGuides

Batch-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.0 accepts 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`ResolutionPer second15-second clip10 clips
wan-3.0480p$0.0394$0.59$5.92
wan-3.0720p$0.0789$1.18$11.84
seedance-2.5720p$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.

A conveyor of small product boxes, each passing under a camera icon and coming out as a short film strip, with a price tag on every strip
One photo in, one 15-second clip out, each with its own quote.

Which errors show up in product-video batches?

What you seeWhyWhat to do
failed with invalid_request, error.input: image_urlRedirect, over 10 MB, GIF or animated WebPFix the photo URL and create again; the hold was released
400 invalid_request on the quoteA field the mode doesn't take, such as aspect_ratioRemove the field or use a Seedance model
410 quote_expiredMore than 12 minutes between quote and createQuote again right before you create
429 concurrency_limit_reached2 generations are already in progressWait the Retry-After seconds and retry
402 daily_spend_limit_reachedThe account hit its daily spend limitResume after 00:00 UTC or ask support
422 idempotency_conflictA key was reused with a different inputUse 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?
Yes, with a Seedance model: Seedance 2.5, 2.0 and 2.0 Fast accept 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?
It's the model's unit price times 15. At price list v12, 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?
No. A clip that fails or times out releases its hold in full. 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.

Models in this post