A working curl + Python guide for catalog packshots, lifestyle, and variants at $0.005 per image.
![Generating Ecommerce Product Images with FLUX.1 [schnell] via the hiapi API](/_next/image?url=https%3A%2F%2Fstatic.hiapi.ai%2Fblog%2F_default%2Flight-cover-12.png&w=3840&q=75)
Catalog photography is the slowest part of launching a SKU online. Studio booking, a stylist, a lightbox, a retoucher — for a $24 mug it makes no sense. flux-schnell/text-to-image on hiapi gets you usable white-background packshots, lifestyle hero shots, and color/material variants for $0.005 per image and roughly 30 seconds end-to-end, which is the regime where you stop thinking about cost per shot and start thinking about prompts and prompt batches.
This guide walks the exact workflow: the four shot types you actually need for a product detail page, the prompt structure that keeps the model on a clean cyclorama instead of inventing a wooden table, how to batch 100 variants through /v1/tasks without melting your terminal, and the small list of things schnell will not do that you should plan around.
flux-schnell for catalog work specificallyThere are higher-fidelity models on hiapi (flux-2, flux-1.1-pro, nano-banana-pro), and for the one hero image on a brand campaign you should use them. For a catalog the math is different:
schnell produces in 1–4 sampling steps, so end-to-end (queue + generation + download) lands around 30 seconds in our runs. flux-2 and nano-banana-pro are several times slower.The tradeoff is real: schnell is weaker on tiny readable text on labels, weaker on photoreal humans holding the product (use nano-banana-pro for that), and noticeably weaker on intricate brand logos. For "show me the product, on a clean background, in three colors, top-down and three-quarter" — its actual job — it is excellent.
Before any prompts, decide what your PDP template needs. A useful starting set:
| Shot type | Aspect ratio | Used on |
|---|---|---|
| White-background packshot | 1:1 | PDP main image, grid thumbnail |
| Three-quarter studio | 4:3 | PDP gallery image 2 |
| Lifestyle / contextual | 3:2 | PDP gallery, social ads |
| Top-down flatlay | 1:1 | PDP gallery, scale reference |
| Color/material variant grid | 1:1 per variant | Color picker thumbnails |
flux-schnell on hiapi supports 1:1, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 16:9, 9:16, 21:9, auto. Anything else gets rejected at task creation.
The bare minimum to confirm your token works. This took 31 seconds end-to-end in my last run:
TOKEN="$HIAPI_API_KEY"
# 1. Create the task
TASK=$(curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-schnell/text-to-image",
"input": {
"prompt": "Product packshot: matte ceramic coffee mug, soft beige glaze, centered on seamless white cyclorama, soft diffused studio lighting, subtle contact shadow, 50mm photography, no props, ecommerce hero, clean negative space top and bottom",
"aspect_ratio": "1:1"
}
}' | jq -r '.data.taskId')
echo "task: $TASK"
# 2. Poll until done (success | fail)
while true; do
STATE=$(curl -s "https://api.hiapi.ai/v1/tasks/$TASK" \
-H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$STATE" | jq -r '.data.status')
echo " status=$STATUS"
[ "$STATUS" = "success" ] && break
[ "$STATUS" = "fail" ] && { echo "$STATE" | jq '.data.error'; exit 1; }
sleep 4
done
# 3. Download (output URLs expire; grab them right away)
URL=$(echo "$STATE" | jq -r '.data.output[0].url')
curl -s -o packshot.jpg "$URL"
echo "saved packshot.jpg"
Important quirk for flux-schnell specifically: the model's input schema accepts aspect_ratio but rejects resolution. Other image models on hiapi (e.g. flux-2, nano-banana-pro) accept both. If you submit {"aspect_ratio": "1:1", "resolution": "1K"} to flux-schnell you'll get:
{
"code": 400,
"message": "additional properties not allowed: resolution"
}
Strip resolution. You don't need it — schnell outputs at a model-default size for the ratio you picked.
A real catalog job submits many tasks, downloads outputs as they finish, and retries the fail cases. This is the script we use for prototyping a SKU range:
"""Batch packshot generator for flux-schnell on hiapi.
Reads a CSV of (sku, prompt, aspect_ratio), submits all jobs, polls in parallel,
downloads finished outputs to ./out/{sku}.jpg. Total cost = $0.005 * len(csv).
"""
import csv
import json
import os
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
API = "https://api.hiapi.ai/v1/tasks"
TOKEN = os.environ["HIAPI_API_KEY"]
OUT = Path("./out"); OUT.mkdir(exist_ok=True)
def _post(path, body):
r = subprocess.run(
["curl", "-s", "-X", "POST", path,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", json.dumps(body)],
capture_output=True, text=True, timeout=60,
)
return json.loads(r.stdout)
def _get(path):
r = subprocess.run(
["curl", "-s", path, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=30,
)
return json.loads(r.stdout)
def render(sku: str, prompt: str, ratio: str) -> tuple[str, str]:
"""Submit → poll → download. Returns (sku, status). status='ok' or 'fail:<reason>'."""
body = {"model": "flux-schnell/text-to-image",
"input": {"prompt": prompt, "aspect_ratio": ratio}}
resp = _post(API, body)
task_id = (resp.get("data") or {}).get("taskId")
if not task_id:
return sku, f"fail:submit:{resp.get('message')}"
deadline = time.time() + 180
while time.time() < deadline:
state = _get(f"{API}/{task_id}")
data = state.get("data") or {}
status = data.get("status")
if status == "success":
url = (data.get("output") or [{}])[0].get("url")
if not url:
return sku, "fail:no-output-url"
subprocess.run(["curl", "-s", "-o", str(OUT / f"{sku}.jpg"), url],
timeout=60, check=True)
return sku, "ok"
if status == "fail":
return sku, f"fail:model:{data.get('error', {}).get('code')}"
time.sleep(4)
return sku, "fail:timeout"
def main():
rows = list(csv.DictReader(open("skus.csv")))
print(f"submitting {len(rows)} renders (cost ${0.005 * len(rows):.3f})")
with ThreadPoolExecutor(max_workers=8) as ex:
futures = {ex.submit(render, r["sku"], r["prompt"], r["aspect_ratio"]): r
for r in rows}
for f in as_completed(futures):
sku, status = f.result()
print(f" {sku}: {status}")
if __name__ == "__main__":
main()
A skus.csv row looks like:
sku,prompt,aspect_ratio
mug-beige-01,"Product packshot: matte ceramic mug, beige glaze, seamless white cyclorama, soft studio lighting, subtle contact shadow, 50mm photography, ecommerce hero, clean negative space",1:1
mug-charcoal-01,"Product packshot: matte ceramic mug, charcoal glaze, seamless white cyclorama, soft studio lighting, subtle contact shadow, 50mm photography, ecommerce hero, clean negative space",1:1
Notes on the production code:
ThreadPoolExecutor(max_workers=8) is a safe parallelism for schnell. Going higher just deepens the queue without finishing sooner.success. The output[].url is a temporary link with an expireAt timestamp; do not store it in your CMS.Catalog prompts are not creative prompts. You want the same lighting, the same background, the same camera, every time, varying only the product. A template:
Product packshot: {MATERIAL} {PRODUCT}, {COLOR} {FINISH},
{COMPOSITION},
{BACKGROUND},
{LIGHTING},
{LENS_NOTE},
ecommerce hero, no props, clean negative space {AREA}
Filled in:
Product packshot: matte ceramic coffee mug, soft beige glaze,
centered single subject filling about 60% of frame,
seamless white cyclorama background,
soft diffused studio lighting from the upper left, subtle contact shadow underneath,
shot on 50mm with shallow depth of field,
ecommerce hero, no props, clean negative space top and bottom
The order matters less than the structure. Things that consistently improve output:
schnell will sometimes zoom way out or push the product to a corner.For lifestyle variants, swap the background + lighting block:
... matte ceramic coffee mug, soft beige glaze, single subject filling about 40% of frame,
on a light oak countertop with morning sunlight from a kitchen window,
soft golden-hour rim lighting, gentle defocused background showing a blurred linen curtain,
shot on 35mm at f/2.8, lifestyle photography, no people, no other props
For top-down flatlay:
... single subject centered, top-down 90-degree angle, ceramic coffee mug, soft beige glaze,
on a light grey paper surface, soft even overhead lighting, soft shadow directly under the rim,
shot on 50mm, flatlay product photography, no props, clean negative space all sides, 1:1
These three prompts, run against the same SKU, give you a usable PDP gallery for the cost of $0.015.
The reason schnell shines for ecommerce is variant generation. Once a prompt works for one color, you mass-produce the rest by string-substituting the color descriptor:
COLORS = [
("beige", "soft beige glaze"),
("charcoal", "deep charcoal matte glaze"),
("sage", "muted sage green glaze"),
("terracotta", "warm terracotta clay glaze"),
("cream", "off-white cream glaze with subtle speckle"),
]
TEMPLATE = (
"Product packshot: matte ceramic coffee mug, {color_desc}, "
"centered single subject filling about 60% of frame, "
"seamless white cyclorama background, soft diffused studio lighting from the upper left, "
"subtle contact shadow underneath, shot on 50mm, ecommerce hero, no props, "
"clean negative space top and bottom"
)
rows = [{"sku": f"mug-{c}-01", "prompt": TEMPLATE.format(color_desc=desc),
"aspect_ratio": "1:1"} for c, desc in COLORS]
Five variants at $0.005 = $0.025 for the entire color picker. The catch: schnell does not perfectly preserve the form factor across renders. The mug handle thickness, the lip curve, the proportion will drift slightly between colors. For a stylized product page that's fine. For pixel-identical variants you need reference-image conditioning, which means flux-1.1-pro with a reference image or nano-banana-pro, not schnell.
The output is a downloaded JPG file. From there, the path to a live PDP depends on your platform:
POST /admin/api/2025-01/products/{id}/images.json with {"image": {"attachment": "<base64>"}}. Returns a CDN URL Shopify hosts.POST /wp-json/wp/v2/media), then PATCH the product with the returned media ID in images: [{id: ...}].In every case, do the upload server-side, not client-side, so your hiapi key never reaches the browser.
At $0.005 per image, the budget is rarely the constraint — but it helps to think in batches:
| Batch | Images | Cost |
|---|---|---|
| One SKU, 4 shots | 4 | $0.02 |
| 50-SKU launch, 4 shots each | 200 | $1.00 |
| 200-SKU catalog refresh | 800 | $4.00 |
| Daily variant testing, 100/day for 30 days | 3,000 | $15.00 |
Token-side cost (the prompt) is free for image endpoints on hiapi — you pay per output image only.
flux-schnell will not do wellThings to plan around before you commit:
schnell, then composite the real label PNG on top in your image editor. Asking schnell to render "with the word 'OAK ROAST' on the mug" produces gibberish about 80% of the time.schnell's weak spot. For lifestyle shots with a person, switch to nano-banana-pro or flux-1.1-pro.For everything else on a PDP — backgrounds, lighting, color variants, contextual shots, mood — flux-schnell is the right tool, in the right price band, with the right turnaround. The whole loop fits inside a while true: poll; download script and a CSV of SKUs, and the cost stays small enough that you can iterate on prompts without budget meetings.
Q: Can I send a reference image to flux-schnell?
A: No. flux-schnell/text-to-image is text-to-image only — the input field accepts prompt and aspect_ratio, nothing else. For reference-conditioned variants, use flux-1.1-pro or nano-banana-pro, both of which accept image inputs on hiapi.
Q: What aspect ratios are supported?
A: 1:1, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 16:9, 9:16, 21:9, and auto. Any other ratio (e.g. 5:7, 2:1) gets rejected at task creation with an error pointing to the supported set.
Q: Why does my task creation 400 when I include resolution?
A: flux-schnell only accepts aspect_ratio in its input. Other image models on hiapi (e.g. flux-2, nano-banana-pro) accept resolution (1K/2K/4K). For schnell, drop it.
Q: How long are the output URLs valid?
A: Each output[].url carries an expireAt timestamp (typically tens of minutes). Download the image bytes as soon as status flips to success and store them in your own bucket — do not hot-link the temp URL into your product database.
Q: Can I run more than 8 concurrent tasks?
A: Yes, but you won't finish faster in practice — schnell is already fast end-to-end and adding parallelism past ~8 just builds queue. For genuinely large batches (thousands), parallelize across multiple worker machines rather than threads in one process.
Q: How do I get pixel-identical variants of the same product?
A: You don't, with schnell alone. Generate the master shot once, then use a reference-image-capable model (flux-1.1-pro or nano-banana-pro) for variant runs with the master as the reference. Cost goes up but form-factor consistency improves dramatically.