Disclosure: I run VELIN (72agi.com), the pay-per-image API used in the code below. The technique (reference-image slots plus submit-then-poll) applies to any Nano Banana Pro endpoint; swap the two HTTP calls if you use another provider.
If you generate product images at any volume, two problems show up fast:
- The product drifts. The bottle gets a different cap, the logo changes, the proportions wobble between scenes.
- Generation is slow. A 2K/4K image with several reference images can take 45-100 seconds, so a synchronous request will time out behind many proxies and serverless platforms.
This post covers both: how to structure reference images so the product stays the same across scenes, and a small resumable batch script that submits, polls, and downloads.
1. Reference images: give each one a job
Nano Banana Pro (Google's gemini-3-pro-image) accepts up to 14 reference images per request. Google's docs split that into up to 6 high-fidelity object references and up to 5 character references; the rest is context. Those numbers are a ceiling, not a target. What works better in practice:
- Start with 2-4 references, not 14. More references only help when each one has a distinct role.
- Put the image that must survive first. For product work, that is the packshot on a clean background (slot 1).
- Assign roles in the prompt: "Keep the product from the first image exactly: shape, colour, logo, proportions. Use the second image only as the lighting/mood reference."
- One scene per request. Change the scene text, keep the references and the "keep the product" sentence identical. That constant part is what gives you consistency across a catalogue.
- Conflicting references get averaged. If your logo file and your packshot disagree, the output will too.
A prompt skeleton I reuse:
Product photo: {scene}.
Keep the product from the first reference image exactly as it is: shape, colour, label text, proportions.
Use the second reference only for {style_role}.
No extra text, no watermark.
None of this is guaranteed: consistency is model behaviour, not an API feature, and small text on labels is still the first thing to drift. Check outputs before you publish them.
2. The async part: submit, poll, download
VELIN's API is two calls (it is not OpenAI-compatible, which is why the code is explicit):
-
POST https://72agi.com/api/generatewith multipart fieldsprompt,model,size(aspect ratio),resolution(1K/2K/4K) and repeatedrefsfiles. Returns202 {"id": "...", "status": "queued"}. -
GET https://72agi.com/api/task/{id}untilstatusissucceeded(you get a site-relativeurland aprice) orfailed(you geterror; failed tasks are not charged).
Auth is Authorization: Bearer <key>. Default rate limit is 8 single generations per minute per account, so a 429 means back off. Results are kept 10 hours, so download right away.
Minimal version (Python, pip install requests):
import os, time, requests
BASE = "https://72agi.com"
H = {"Authorization": f"Bearer {os.environ['VELIN_API_KEY']}"}
def generate(prompt, refs, model="nano-banana-pro", size="1:1", resolution="2K", out="out.png"):
# multipart/form-data: send plain fields through files= as (None, value)
fields = [(k, (None, v)) for k, v in
dict(prompt=prompt, model=model, size=size, resolution=resolution).items()]
fields += [("refs", (os.path.basename(p), open(p, "rb"))) for p in refs] # up to 14
r = requests.post(f"{BASE}/api/generate", headers=H, files=fields, timeout=120)
r.raise_for_status() # 401 bad key, 402 no credits, 429 slow down
task = r.json()["id"]
while True:
time.sleep(4)
j = requests.get(f"{BASE}/api/task/{task}", headers=H, timeout=30).json()
if j["status"] == "failed":
raise RuntimeError(j.get("error"))
if j["status"] == "succeeded":
break
open(out, "wb").write(requests.get(BASE + j["url"], headers=H, timeout=120).content)
return out
3. A batch that survives crashes
For a catalogue you want three more things: spacing submissions under the rate limit, polling several tasks in parallel, and not paying twice if the script dies halfway. The trick is a tiny manifest file that stores the task id as soon as it exists:
manifest = json.load(open("shots/manifest.json")) if os.path.exists("shots/manifest.json") else {}
def work(i, scene):
m = manifest.setdefault(f"{i:03d}", {"scene": scene})
if m.get("file") and os.path.exists(m["file"]):
return # already done
if not m.get("task"): # only submit once
m["task"] = submit(scene) # spaced ~7.6 s apart (8/min)
save(manifest)
result = poll(m["task"]) # resume polling an old task id after a crash
...
Task ids survive client disconnects, so re-running the script re-attaches to tasks that are still running instead of creating duplicates. The full script (product image is always reference #1, optional extra references, worker threads, 429 handling, manifest) is here:
Run it like this:
export VELIN_API_KEY=...
python examples/batch_product_shots.py product.png --scenes scenes.txt --out shots/ \
--model nano-banana-pro --resolution 2K --ref style.png
scenes.txt is one scene per line ("on a marble kitchen counter, soft morning light", "flat lay on linen, top-down"...).
Things I'd check before using this for real
- Cost per image. VELIN is a flat ~$0.037/image at 1K, 2K and 4K, and failed generations are not charged. Compare against your actual resolution mix and volume; at 1K other providers can be cheaper.
- Content moderation follows the upstream model providers, so some prompts and references will simply fail (and not be charged).
- Backend only. The API sends no CORS headers, so call it from a server or script, not from browser JavaScript. Keep the key out of front-end code.
- Top-up is USDT for now; cards are marked "coming soon".
Honest test notes
I did not include sample outputs here. The scripts in the repo are tested end to end against a local mock that follows the documented API contract (the repo's TESTING.md lists what was tested against what, including n8n and ComfyUI), and the public endpoints were checked against the live API. Judge output quality on your own products.
If you want to try it with your own prompts, message me on Telegram @ayan8866e for a free test (there are no automatic free credits). Examples for Python, Node, curl, n8n, SillyTavern and a draft ComfyUI node: https://github.com/velin-api/velin-image-api
Questions or corrections are welcome in the comments.
Top comments (0)