Batch product shots.

Many variations across seeds and aspect ratios, run concurrently, with a cost check up front and safe failure handling.

Lifestyle photograph of a canvas tote bag hanging on a bicycle handlebar at a flower market, morning light
Lifestyle photograph of a canvas tote bag hanging on a bicycle handlebar at a flower market, morning lightZ-Image Turbo · seed 11
Stack
Python 3.10+, asyncio, httpx
Needs
API key that can read billing
Uses
image.generate, estimate, idempotency keys, rate limits

A batch has to survive its own failures.

Twelve variations means twelve submits, and any of them can time out after the API has already accepted it. A plain retry then creates and bills a second job.

The script gives each variation an Idempotency-Key, writes it to a manifest before the first attempt, and reuses it on every retry. It also prices the whole batch up front and backs off on 429, so the batch can be re-run after a crash without paying twice.

How it fits together.

The recipe in order, from the first request to the saved file.

  1. Build the grid

    One prompt across three seeds and four aspect ratios (1:1, 4:5, 9:16, 16:9), each keyed in manifest.json.

  2. Estimate the batch

    One POST /v1/generation/estimate per aspect ratio, multiplied out and checked against your balance before anything is submitted.

  3. Submit with a saved key

    Each variation gets a UUID key, saved to the manifest before the POST. Every retry of that variation reuses it.

  4. Run a few at once

    A semaphore keeps jobs in flight at or below your tier's concurrency limit, which is 5 on tier 1.

  5. Poll, save, record

    Poll with backoff, save each PNG, and write state and credits back to the manifest. A second run skips what's done.

The core of it.

An excerpt from the recipe. The full version runs as-is.

batch_shots.py, excerptpython
async def submit(api, entry, manifest, stop) -> bool:    if not entry.get("idempotency_key"):        entry["idempotency_key"] = str(uuid.uuid4())        save(manifest)  # persist the key before the first attempt    body = {"prompt": PROMPT, "aspect_ratio": entry["aspect_ratio"],            "resolution": RESOLUTION, "seed": entry["seed"]}    headers = {"Idempotency-Key": entry["idempotency_key"]}    for attempt in range(SUBMIT_TRIES):        try:            job = await call(api, "POST", "/generation/capabilities/image.generate",                             json=body, headers=headers)        except httpx.TransportError:            await asyncio.sleep(2 ** attempt)  # the job may exist; the same key finds it            continue        except APIError as exc:            if exc.status == 429 or exc.code == "idempotency.in_progress" or exc.status >= 500:                await asyncio.sleep(exc.retry_after or 2 ** attempt)                continue            if exc.code == "billing.insufficient_credits":                stop.set()  # stop submitting the rest of the batch            return False  # other 4xx: nothing was created or billed        entry.update(request_id=job["request_id"], state="queued")        return True    return False  # still unknown: the next run retries with the same key

The full script adds the manifest, the estimate step, polling, downloads and a one-time retry for jobs that end failed.

Full recipe in the docs

One product, many looks.

Real Z-Image Turbo outputs, each with the prompt and seed that made it: one prompt in three colours, then six looks.

  • Studio product photograph of a canvas tote bag in sand beige on a seamless light gray backdrop, soft shadow

    sand beige

  • Studio product photograph of a canvas tote bag in deep teal on a seamless light gray backdrop, soft shadow

    deep teal

  • Studio product photograph of a canvas tote bag in coral on a seamless light gray backdrop, soft shadow

    coral

Studio product photograph of a canvas tote bag in {colour} on a seamless light gray backdrop, soft shadowZ-Image Turbo · seed 11 · 1:1
  • Studio

    StudioStudio product photograph of a ceramic table lamp with a linen shade on a…Z-Image Turbo · seed 11

  • Lifestyle

    LifestyleThe same style ceramic table lamp glowing on a bedside table in a calm bedroom at…Z-Image Turbo · seed 42

  • Flat lay

    Flat layFlat lay of summer travel essentials, sunglasses, passport, film camera, linen…Z-Image Turbo · seed 11

  • Splash

    SplashA bottle of sparkling water frozen in a splash of water and lime slices, bright…Z-Image Turbo · seed 11

  • Outdoor

    OutdoorA hiking backpack resting on a granite boulder above a misty valley at sunrise…Z-Image Turbo · seed 42

  • Macro

    MacroMacro product photograph of a mechanical watch movement with gears and rubies…Z-Image Turbo · seed 11

What it does when things go wrong.

Every outcome of a submit or a job, and whether it costs anything.

From the failure table in the full recipe.
What happenedBilled?What the script does
Network error, timeout or 5xx on submit
Maybe
Retries with the same key. If the first attempt created a job, the retry returns it with Idempotent-Replayed: true.
409 idempotency.in_progress
Held by the first attempt
Waits Retry-After (1 second) and retries with the same key.
429 on submit
No
Waits Retry-After and retries with the same key.
402 or 422 on submit
No
Records the error. On 402 it stops submitting the rest of the batch.
Job ended failed
No
The hold is released. Retries once with a new key, then records the error.
Job ended nsfw
No
Records it. Change the prompt if it keeps happening.
Script killed mid-batch
Only created jobs
The next run polls saved request ids and retries the rest with their saved keys.

What a run costs.

Priced per output at the tier the recipe uses. The price is fixed when each job is submitted.

$0.048a run, or 0.96 credits

At current prices. 1 credit = $0.05. Jobs that end failed, nsfw or canceled cost nothing.
StepModelTierCountCreditsUSD
Variations, 3 seeds × 4 aspect ratios
1k
12 × 0.08
0.96
$0.048
Total
0.96
$0.048

Questions.

Why separate jobs instead of num_outputs?

You can use num_outputs: image.generate returns up to 4 images per job on z-image-turbo, priced per image. Separate jobs with separate seeds make each image reproducible on its own, which matters when you need to regenerate one specific shot.

What if the script crashes halfway?

Run it again within 24 hours. Saved request ids are polled rather than resubmitted, and unfinished submits retry with their saved idempotency keys, so nothing is billed twice. Idempotency keys expire after 24 hours.

How many jobs can run at once?

It depends on your rate-limit tier: 5 concurrent jobs and 60 submits per minute on tier 1, rising with lifetime credit purchases. A submit over the concurrency limit gets 429 and is not charged.

Start yours.

Request early access. We’ll email you when your account is approved, with 200 free credits to spend in the first 30 days.