How to Generate Product Images in Bulk with the Seedream 5.0 Pro API

Build a CSV-driven Seedream 5.0 Pro API workflow with multi-reference inputs, controlled concurrency, retries, cost estimates, and human review.

How to Generate Product Images in Bulk with the Seedream 5.0 Pro API

A 100-SKU catalog is not one image request. Each product needs its own reference images, prompt variables, filename, retry record, and approval state. Copying prompts into a playground can produce images, but it cannot tell you which version belongs to MUG-001 or whether a failed bag image should be retried.

The practical solution is one Seedream request per SKU, run through a small worker pool. That is what “bulk” means here. BytePlus currently lists text-to-multiple-images as unsupported for Seedream 5.0 Pro, so one prompt does not return an entire catalog. Instead, a CSV supplies the product data, Python sends several independent requests at a controlled rate, and a manifest records every result.

The finished workflow uses the Seedream 5.0 Pro API on GPTProto and produces:

  • products.csv, with one row per product image;

  • bulk_seedream_products.py, which creates and runs the jobs;

  • generated/, with filenames tied to SKUs; and

  • manifest.json, with attempts, errors, output locations, and review status.

Table of contents

What “Seedream 5.0 Pro API Batch” Actually Means

Three phrases are often treated as synonyms. They are not.

Term Meaning Current behavior
Multi-reference input Several source images guide one edit The GPT Proto image-edit endpoint accepts up to 10 references
Multiple outputs One prompt returns several separate images Not exposed for Seedream 5.0 Pro
Bulk generation Many SKU-specific requests run through a queue The workflow built below

Multi-reference input improves one result. It does not create ten results. A product front view, a material close-up, and a lighting reference can all guide one final image. To create images for 100 products, the client still sends 100 requests.

That distinction affects everything downstream. One SKU per request gives each output an owner, filename, cost record, and retry history. Concurrency makes the queue move faster; it does not turn the model into a multi-output endpoint. The underlying model behavior is documented in BytePlus’s image-generation matrix, while GPT Proto exposes the model through its own /api/v3/ routes.

What You Will Build

The data moves through five stages:

CSV catalog → prompt and references → controlled request pool → downloaded files → human review

The script uses synchronous requests with enable_sync_mode: true. That choice is deliberate. GPT Proto’s live model page documents an asynchronous result_id workflow, but the complete status and output response schema is not published on that page. A production tutorial should not invent those fields.

Synchronous mode still supports bulk generation. Each worker handles one product from request through download, while ThreadPoolExecutor lets a small number of workers run at the same time. The manifest is written by the main thread after each worker finishes, so interrupted runs can skip files that are already present.

For a later high-throughput version, set enable_sync_mode to false only after you have captured a live response and confirmed the current task ID, status, output, and error fields. Then store the returned result_id before polling /api/v3/predictions/{result_id}/result.

What You Need Before Starting

Install Python and the single dependency:

python --version
python -m pip install requests

Create a GPT Proto API key, then keep it in an environment variable:

export GPTPROTO_API_KEY="your-key-here"

The current live API examples use this header format:

Authorization: Bearer YOUR_API_KEY

You also need product references at public HTTPS URLs. A path such as /Users/me/photos/mug.jpg exists only on your computer. The remote API cannot fetch it. Put the files on a CDN, object-storage bucket, or another HTTPS location that the API can reach without a login cookie.

Start with a small catalog slice. Pick products with different shapes, materials, and label complexity. A simple mug can pass while a bag with multiple straps fails, so one easy test is not enough to approve a shared prompt.

Estimate the Batch Cost Before You Run It

GPT Proto’s live Seedream 5.0 Pro pricing was checked on September 17, 2026. Use the model page for per-image rates and the GPT Proto pricing page for account-level pricing information.

Output Base price, including the first reference Each additional reference
1K $0.0405 per image +$0.0027
2K $0.0810 per image +$0.0027

The current Seedream 5.0 Pro Image Edit API accepts up to 10 reference images. The first is included in the base price. References two through ten add $0.0027 each.

That produces these planning figures before retries or rejected generations:

Batch Estimated API cost
100 one-reference images at 1K $4.05
100 one-reference images at 2K $8.10
1,000 one-reference images at 1K $40.50
1,000 one-reference images at 2K $81.00
100 images with three references at 1K $4.59
100 images with three references at 2K $8.64

The raw-generation bill is easy to estimate. The approved-image cost is not. If a product needs several attempts, every generated image counts. Use this formula for a more realistic budget:

estimated catalog cost =
  number of SKUs
  × average attempts per approved image
  × (base output price + paid reference-image cost)

Use 1K to test the prompt system across representative products. Do not treat it as a guaranteed preview of a later 2K call. The 2K request creates a new image, so composition and details can change. Review the final-resolution result again.

Step 1: Create a Product CSV

Save this as products.csv:

sku,name,color,material,angle,background,product_image_url,detail_image_url,style_reference_url,avoid
MUG-001,Stackable coffee mug,matte navy,stoneware,three-quarter,soft warm gray,https://cdn.example.com/mug-front.jpg,https://cdn.example.com/mug-glaze.jpg,,steam or hands
BAG-014,Compact crossbody bag,forest green,pebbled leather,front three-quarter,light beige,https://cdn.example.com/bag-front.jpg,https://cdn.example.com/bag-texture.jpg,https://cdn.example.com/style-neutral.jpg,model or extra straps

Replace every cdn.example.com URL before running the script. The CSV fields serve different jobs:

  • sku is the permanent product key. It also becomes part of the output filename.

  • name, color, material, angle, and background hold facts that change by product.

  • product_image_url is the identity reference and is required.

  • detail_image_url can show material, controls, hardware, or a label close-up.

  • style_reference_url controls lighting and background treatment, not product identity.

  • avoid lists a few visible mistakes that matter for that SKU.

Keep values short and factual. Do not put an entire creative brief into one cell. Do not put the API key in the CSV, source code, or manifest.

The script below rejects duplicate SKUs, missing product URLs, non-HTTPS references, and more than ten references before sending a paid request.

Step 2: Give Each Reference Image One Job

A Seedream 5.0 Pro API multi-reference image request works better when the prompt explains why each image exists. Use a stable order:

  1. Product identity reference: silhouette, proportions, visible design, and packaging.

  2. Detail reference: material, hardware, controls, or label treatment.

  3. Style reference: lighting, surface, palette, and background only.

The generated prompt will include instructions such as:

Image 1 is the product identity reference. Preserve its silhouette, proportions, visible design, and packaging. Image 2 is the detail reference. Use it for material and hardware details. Image 3 is the lighting and background reference only. Do not copy objects from it.

Start with one to three useful references. Ten unlabeled inputs cost more and can conflict with one another. More images are not automatically more accurate.

A ComfyUI launch post describes stable product features across scene variations as a production use case. Treat that as a workflow claim, not an independent benchmark. Product identity still needs a side-by-side review against the source.

Step 3: Build One Reusable Product Prompt

Keep catalog-wide rules in code and SKU facts in the CSV. A useful order is:

asset type
→ product identity and protected details
→ requested background
→ camera and composition
→ lighting and shadow
→ material behavior
→ exclusions

For the mug row, the script produces instructions close to this:

Create one square e-commerce hero photograph of the Stackable coffee mug.
The product is matte navy stoneware. Preserve the product identity, silhouette,
proportions, visible design, color family, material, and packaging details from
the references. Show a three-quarter view on a soft warm gray background.
Use a large diffused key light from the upper left and a natural contact shadow.
Keep the whole product inside the frame with crop-safe space. Return one product
image, not a collage or contact sheet. Do not add steam or hands. Do not invent
logos, labels, accessories, duplicate products, handles, straps, or controls.

Name observable details. “Premium quality” gives the model little physical information. “Soft contact shadow falling back-right” does. Keep exclusions short because the current GPT Proto request does not expose a separate negative_prompt field.

For more prompt patterns, use the Seedream 5.0 Pro Prompt Guide.

Step 4: Test One Product Before Starting the Queue

Run one synchronous image-edit request first:

curl --request POST \
  --url "https://gptproto.com/api/v3/doubao/dola-seedream-5-0-pro-260628/image-edit" \
  --header "Authorization: Bearer $GPTPROTO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "images": [
      "https://your-cdn.example.com/mug-front.jpg",
      "https://your-cdn.example.com/mug-glaze.jpg"
    ],
    "prompt": "Create one square e-commerce hero photograph of the referenced mug. Image 1 is the product identity reference. Preserve its silhouette, proportions, handle, color, and visible design. Image 2 is the glaze detail reference. Show a three-quarter view on a soft warm gray background with a natural contact shadow. Keep the entire product inside the frame. No hands, steam, extra text, duplicate products, or collage.",
    "size": "1024x1024",
    "enable_base64_output": false,
    "enable_sync_mode": true,
    "output_format": "png"
  }'

The endpoint and current Bearer header match the live GPT Proto model example. Print and retain the first successful JSON response before adding more automation. Check that it contains an output your client can download.

Do not continue because the image merely looks attractive. Compare it with the references:

  • Is the silhouette recognizable?

  • Are the color and material acceptably close?

  • Did the model add or remove a handle, strap, button, cap, or label?

  • Is required text correct character by character?

  • Does the product make physical contact with the surface?

  • Is there enough crop-safe space for the intended marketplace placement?

Test one representative product and one difficult product. Both should pass before the full queue starts.

Step 5: Run One Request per SKU

The batch script sends the same image-edit payload used in the cURL test. The variables come from each CSV row. MAX_WORKERS controls how many independent requests can be active at once.

Start with a small value such as 2 or 3. The BytePlus documentation may publish limits for direct BytePlus accounts, but those numbers are not automatically your GPT Proto account limits. If the API returns 429, reduce the worker count and follow the response’s retry guidance.

The script uses deterministic names:

{sku}_hero_{attempt:02d}.{extension}

Examples include MUG-001_hero_01.png and BAG-014_hero_02.png. The attempt remains visible, so a reviewer can trace the selected file back to the manifest.

Step 6: Add Retries and Resume Support

Not every failure should trigger another paid request.

The script retries explicit 429 and 5xx responses with exponential backoff and jitter. It also honors a numeric Retry-After header when present. Authentication, invalid-parameter, balance, and moderation errors are recorded without an unchanged resubmission.

Read timeouts and dropped connections are different. The server may have accepted the request even though the client did not receive the response. Without a documented idempotency key, an automatic resubmission could create a second image and a second charge. The script marks those cases manual_check instead. That is slower than blind retrying. It is also safer for a production budget.

After each worker finishes, the main thread updates manifest.json through an atomic file replacement. On restart:

  • an item with downloaded, approved, or manual_review status and an existing local file is skipped;

  • a failed_retryable item continues only if attempts remain;

  • a manual_check item is not submitted again automatically; and

  • a failed item retains its error and is skipped until an operator fixes the cause, then removes that SKU entry from the manifest or changes its status to failed_retryable.

This synchronous design cannot save an asynchronous task ID. If you later adopt the documented background workflow, add task_id to the same manifest and save the live result_id immediately after submission.

Complete Python Script for Bulk Seedream Product Images

Save the following as bulk_seedream_products.py beside products.csv.

from __future__ import annotations

import base64
import csv
import json
import os
import random
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import requests

API_URL = (
    "https://gptproto.com/api/v3/doubao/"
    "dola-seedream-5-0-pro-260628/image-edit"
)
CSV_PATH = Path(os.getenv("PRODUCTS_CSV", "products.csv"))
OUTPUT_DIR = Path(os.getenv("OUTPUT_DIR", "generated"))
MANIFEST_PATH = Path(os.getenv("MANIFEST_PATH", "manifest.json"))

SIZE = os.getenv("SEEDREAM_SIZE", "1024x1024")
OUTPUT_FORMAT = os.getenv("SEEDREAM_OUTPUT_FORMAT", "png")
MAX_WORKERS = int(os.getenv("MAX_WORKERS", "3"))
MAX_ATTEMPTS = int(os.getenv("MAX_ATTEMPTS", "4"))
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "300"))
PROMPT_VERSION = os.getenv("PROMPT_VERSION", "product-hero-v1")

REFERENCE_FIELDS = (
    (
        "product_image_url",
        "the product identity reference. Preserve its silhouette, proportions, "
        "visible design, color family, material, and packaging",
    ),
    (
        "detail_image_url",
        "the detail reference. Use it for material, hardware, controls, and label details",
    ),
    (
        "style_reference_url",
        "the lighting and background reference only. Do not copy products or props from it",
    ),
)

def now_utc() -> str:
    return datetime.now(timezone.utc).isoformat()

def is_https_url(value: str) -> bool:
    parsed = urlparse(value)
    return parsed.scheme == "https" and bool(parsed.netloc)

def clean_filename(value: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
    return cleaned.strip("-._") or "product"

def reference_data(row: dict[str, str]) -> tuple[list[str], list[str]]:
    urls: list[str] = []
    role_lines: list[str] = []
    for field, role in REFERENCE_FIELDS:
        url = row.get(field, "").strip()
        if not url:
            continue
        urls.append(url)
        role_lines.append(f"Image {len(urls)} is {role}.")
    return urls, role_lines

def validate_and_load_rows(path: Path) -> list[dict[str, str]]:
    required = {
        "sku",
        "name",
        "color",
        "material",
        "angle",
        "background",
        "product_image_url",
        "detail_image_url",
        "style_reference_url",
        "avoid",
    }

    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        fields = set(reader.fieldnames or [])
        missing = required - fields
        if missing:
            raise ValueError(f"CSV is missing columns: {sorted(missing)}")

        rows: list[dict[str, str]] = []
        seen_skus: set[str] = set()

        for line_number, raw_row in enumerate(reader, start=2):
            row = {key: (value or "").strip() for key, value in raw_row.items()}
            sku = row["sku"]
            if not sku:
                raise ValueError(f"Line {line_number}: sku is required")
            if sku in seen_skus:
                raise ValueError(f"Line {line_number}: duplicate sku {sku!r}")
            seen_skus.add(sku)

            urls, _ = reference_data(row)
            if not row["product_image_url"]:
                raise ValueError(
                    f"Line {line_number}: product_image_url is required"
                )
            if len(urls) > 10:
                raise ValueError(
                    f"Line {line_number}: {sku} has more than 10 references"
                )
            invalid_urls = [url for url in urls if not is_https_url(url)]
            if invalid_urls:
                raise ValueError(
                    f"Line {line_number}: references must be public HTTPS URLs: "
                    f"{invalid_urls}"
                )
            rows.append(row)

    if not rows:
        raise ValueError("The CSV contains no product rows")
    return rows

def build_prompt(row: dict[str, str], role_lines: list[str]) -> str:
    avoid = row.get("avoid") or "extra products or unrelated props"
    references = " ".join(role_lines)
    return (
        f"Create one square e-commerce hero photograph of the {row['name']}. "
        f"{references} "
        f"The product is {row['color']} {row['material']}. "
        "Preserve the product identity, silhouette, proportions, visible design, "
        "color family, material, and packaging details from the references. "
        f"Show a {row['angle']} view on a {row['background']} background. "
        "Use a large diffused key light from the upper left and a natural contact "
        "shadow falling back-right. Keep the whole product inside the frame with "
        "crop-safe space. Return one product image, not a collage or contact sheet. "
        f"Do not add {avoid}. Do not invent logos, labels, accessories, duplicate "
        "products, handles, straps, buttons, caps, or controls."
    )

def load_manifest(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {"prompt_version": PROMPT_VERSION, "items": {}}
    with path.open("r", encoding="utf-8") as handle:
        data = json.load(handle)
    if not isinstance(data.get("items"), dict):
        raise ValueError("manifest.json must contain an object named 'items'")
    return data

def save_manifest(path: Path, manifest: dict[str, Any]) -> None:
    temporary = path.with_suffix(path.suffix + ".tmp")
    with temporary.open("w", encoding="utf-8") as handle:
        json.dump(manifest, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
    os.replace(temporary, path)

def output_items(payload: dict[str, Any]) -> list[Any]:
    data = payload.get("data")
    containers = [data, payload] if isinstance(data, dict) else [payload]
    for container in containers:
        for key in ("outputs", "output", "images", "image"):
            value = container.get(key)
            if value:
                return value if isinstance(value, list) else [value]
    raise ValueError(
        "No output field found. Inspect the saved response and update output_items() "
        "to match the current API response."
    )

def output_value(item: Any) -> str:
    if isinstance(item, str):
        return item
    if isinstance(item, dict):
        for key in ("url", "image_url", "b64_json", "base64"):
            value = item.get(key)
            if isinstance(value, str) and value:
                return value
    raise ValueError(f"Unsupported output item: {item!r}")

def extension_from_content_type(content_type: str) -> str:
    mime = content_type.split(";", 1)[0].strip().lower()
    return {
        "image/png": ".png",
        "image/jpeg": ".jpg",
        "image/webp": ".webp",
    }.get(mime, f".{OUTPUT_FORMAT}")

def save_output(value: str, file_stem: Path) -> tuple[Path, str | None]:
    if value.startswith("data:image/"):
        header, encoded = value.split(",", 1)
        mime = header.split(";", 1)[0].removeprefix("data:")
        destination = file_stem.with_suffix(extension_from_content_type(mime))
        destination.write_bytes(base64.b64decode(encoded))
        return destination, None

    if value.startswith("https://") or value.startswith("http://"):
        response = requests.get(value, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        extension = extension_from_content_type(
            response.headers.get("Content-Type", "")
        )
        destination = file_stem.with_suffix(extension)
        destination.write_bytes(response.content)
        return destination, value

# Some responses return raw Base64 without a data URI.
    try:
        decoded = base64.b64decode(value, validate=True)
    except ValueError as exc:
        raise ValueError("Output is neither a URL nor valid Base64") from exc
    destination = file_stem.with_suffix(f".{OUTPUT_FORMAT}")
    destination.write_bytes(decoded)
    return destination, None

def backoff_seconds(attempt: int, response: requests.Response | None) -> float:
    if response is not None:
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            try:
                return max(0.0, float(retry_after))
            except ValueError:
                pass
    return min(60.0, (2 ** (attempt - 1)) + random.uniform(0.0, 1.0))

def result_record(
    row: dict[str, str],
    references: list[str],
    status: str,
    attempt: int,
    *,
    output_url: str | None = None,
    local_path: str | None = None,
    error: str | None = None,
    created_at: str | None = None,
) -> dict[str, Any]:
    timestamp = now_utc()
    return {
        "sku": row["sku"],
        "asset_type": "hero",
        "status": status,
        "attempt": attempt,
        "prompt_version": PROMPT_VERSION,
        "reference_urls": references,
        "output_url": output_url,
        "local_path": local_path,
        "error": error,
        "requester": "",
        "reviewer": "",
        "approval_status": "pending" if status == "downloaded" else "",
        "created_at": created_at or timestamp,
        "updated_at": timestamp,
    }

def process_product(
    row: dict[str, str], previous: dict[str, Any] | None
) -> dict[str, Any]:
    api_key = os.environ["GPTPROTO_API_KEY"]
    references, role_lines = reference_data(row)
    prompt = build_prompt(row, role_lines)
    created_at = (previous or {}).get("created_at") or now_utc()
    first_attempt = int((previous or {}).get("attempt", 0)) + 1

    if first_attempt > MAX_ATTEMPTS:
        return result_record(
            row,
            references,
            "failed",
            first_attempt - 1,
            error="Maximum attempts already reached",
            created_at=created_at,
        )

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    body = {
        "images": references,
        "prompt": prompt,
        "size": SIZE,
        "enable_base64_output": False,
        "enable_sync_mode": True,
        "output_format": OUTPUT_FORMAT,
    }

    for attempt in range(first_attempt, MAX_ATTEMPTS + 1):
        response: requests.Response | None = None
        try:
            response = requests.post(
                API_URL,
                headers=headers,
                json=body,
                timeout=REQUEST_TIMEOUT,
            )

            if response.status_code == 429 or response.status_code >= 500:
                message = f"HTTP {response.status_code}: {response.text[:500]}"
                if attempt == MAX_ATTEMPTS:
                    return result_record(
                        row,
                        references,
                        "failed_retryable",
                        attempt,
                        error=message,
                        created_at=created_at,
                    )
                time.sleep(backoff_seconds(attempt, response))
                continue

            response.raise_for_status()
            payload = response.json()
            item = output_items(payload)[0]
            value = output_value(item)
            file_stem = OUTPUT_DIR / (
                f"{clean_filename(row['sku'])}_hero_{attempt:02d}"
            )
            local_file, output_url = save_output(value, file_stem)
            return result_record(
                row,
                references,
                "downloaded",
                attempt,
                output_url=output_url,
                local_path=str(local_file),
                created_at=created_at,
            )

        except requests.exceptions.ConnectTimeout as exc:

# No connection was established; a bounded retry is reasonable.
            if attempt == MAX_ATTEMPTS:
                return result_record(
                    row,
                    references,
                    "failed_retryable",
                    attempt,
                    error=str(exc),
                    created_at=created_at,
                )
            time.sleep(backoff_seconds(attempt, response))

        except (
            requests.exceptions.ReadTimeout,
            requests.exceptions.ConnectionError,
        ) as exc:

# The server may have accepted the request. Avoid a blind resubmission.
            return result_record(
                row,
                references,
                "manual_check",
                attempt,
                error=f"Ambiguous network failure; verify account history: {exc}",
                created_at=created_at,
            )

        except requests.exceptions.HTTPError as exc:
            status_code = response.status_code if response is not None else "unknown"
            details = response.text[:500] if response is not None else str(exc)
            return result_record(
                row,
                references,
                "failed",
                attempt,
                error=f"HTTP {status_code}: {details}",
                created_at=created_at,
            )

        except (ValueError, KeyError, json.JSONDecodeError) as exc:
            return result_record(
                row,
                references,
                "manual_check",
                attempt,
                error=str(exc),
                created_at=created_at,
            )

    return result_record(
        row,
        references,
        "failed",
        MAX_ATTEMPTS,
        error="Request ended without a result",
        created_at=created_at,
    )

def should_skip(entry: dict[str, Any] | None) -> bool:
    if not entry:
        return False
    if entry.get("status") in {"manual_check", "failed"}:
        return True
    if entry.get("status") not in {"downloaded", "manual_review", "approved"}:
        return False
    local_path = entry.get("local_path")
    return bool(local_path and Path(local_path).exists())

def main() -> None:
    if not os.getenv("GPTPROTO_API_KEY"):
        raise SystemExit("Set GPTPROTO_API_KEY before running the script")
    if MAX_WORKERS < 1:
        raise SystemExit("MAX_WORKERS must be at least 1")

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    rows = validate_and_load_rows(CSV_PATH)
    manifest = load_manifest(MANIFEST_PATH)
    items: dict[str, Any] = manifest["items"]

    pending: list[dict[str, str]] = []
    for row in rows:
        entry = items.get(row["sku"])
        if should_skip(entry):
            print(f"SKIP {row['sku']}: {entry['status']}")
        else:
            pending.append(row)

    if not pending:
        print("Nothing to submit")
        return

    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        future_to_row = {
            executor.submit(process_product, row, items.get(row["sku"])): row
            for row in pending
        }
        for future in as_completed(future_to_row):
            row = future_to_row[future]
            try:
                record = future.result()
            except Exception as exc:  # Preserve an unexpected worker failure.
                references, _ = reference_data(row)
                record = result_record(
                    row,
                    references,
                    "failed",
                    int(items.get(row["sku"], {}).get("attempt", 0)),
                    error=f"Unexpected worker error: {exc}",
                )
            items[row["sku"]] = record
            manifest["prompt_version"] = PROMPT_VERSION
            manifest["updated_at"] = now_utc()
            save_manifest(MANIFEST_PATH, manifest)
            print(f"{record['status'].upper()} {row['sku']}: {record.get('local_path') or record.get('error')}")

if __name__ == "__main__":
    main()

Run it with:

python bulk_seedream_products.py

You can change the worker count and output size without editing the file:

MAX_WORKERS=2 SEEDREAM_SIZE=2048x2048 python bulk_seedream_products.py

The console reports each SKU as it finishes. Open manifest.json to see which files are downloaded, failed, or held for a manual check. If the live response uses a field shape not covered by output_items(), the script records the mismatch instead of silently treating the job as complete. Update that small parser using the JSON captured during the single-product test.

Review Every Image Before It Reaches the Store

A downloaded image is not an approved image. Keep generation status and approval status separate.

Check Common failure
Geometry Changed cap, strap, button, handle, or proportions
Color Wrong shade, saturation, or surface finish
Material Plastic replaces metal; leather texture becomes flat
Packaging Invented logo, misspelled label, or changed volume
Scene Floating object or implausible contact shadow
Crop Product is clipped in marketplace thumbnails
Duplication An extra product or accessory appears

Move a record from pending to approved, rejected, or needs_edit only after a person compares the output with the source images. If label copy must be legally exact, keep the source label in post-production rather than trusting generated text without verification.

For a first-party quality benchmark, run a small matrix before scaling:

  • a labeled bottle across a catalog background, lifestyle scene, and campaign layout;

  • a leather bag across the same three scene types; and

  • a lamp with geometry and control details visible.

Record first-pass approval rate, attempts per approved image, identity failures, label errors, and API cost per approved image. Those measurements describe your catalog better than a generic model demo.

Make the Workflow Usable by a Team

The manifest is the handoff contract between the operator, designer, and reviewer. Keep these fields even if one person currently does every job:

Field Why it matters
sku Permanent primary key for the product
prompt_version Identifies the shared rules used for the output
reference_urls Reconstructs the exact source set
attempt Separates reruns and supports cost analysis
requester and reviewer Assigns ownership
approval_status Keeps generated and publishable assets distinct
output_url and local_path Connects the API result to the stored file
error Preserves the reason a row stopped
timestamps Shows when the record changed

Treat the SKU as immutable. If the shared prompt changes, increment PROMPT_VERSION rather than overwriting the history. Rerun rejected rows, not the whole catalog. Store approved assets separately from drafts, and never commit an API key to the same repository.

This is the difference between a batch script and a team workflow. The script produces files. The records explain how those files were produced and whether they are allowed to ship.

Common Problems and Fixes

Problem Likely cause Fix
401 or 403 Key, header, account access, or balance Confirm the current Bearer header on the live model page and check the account
Reference does not load Private, local, expired, or non-HTTPS URL Use a stable public HTTPS URL and test it outside your signed-in browser
429 Too many concurrent requests Lower MAX_WORKERS; honor the documented retry response
Request returns a task but no image Asynchronous mode is active Use enable_sync_mode: true for this script, or implement polling from a verified live response
Possible duplicate after timeout Client lost a response after submission Do not blindly retry; check account history and mark the row manually
Product changes too much Reference roles or protected details are vague Name the identity reference and list the visible parts that must remain
Extra objects or collage Deliverable is underspecified Ask for one product image and explicitly reject collages and duplicates
Label text is wrong Generative rendering is not exact Check every character; preserve exact regulated text in post-production
Output parser finds no image Current JSON shape differs Inspect the single-test response and update output_items()

Do not add retries until you know whether the failure happened before or after the server accepted the request. Reliability is not the same as resubmitting everything.

When Seedream 5.0 Pro Is the Right Choice

Seedream 5.0 Pro fits product lifestyle scenes, campaign layouts, localized creative, and catalog variations guided by reference images. Its flat per-image tiers also make the raw generation budget straightforward to calculate.

It is not a substitute for product photography or post-production when the output must preserve pixel-identical geometry, legally exact packaging, or regulated label copy. It is also the wrong fit for a system that publishes every generated image without review, or for a requirement that one request return many separate product files.

Use it for scalable creative production. Keep a person between generation and publication.

Start with a Representative Catalog Slice

Choose a small set of SKUs that covers different shapes, materials, labels, and accessories. Lock the CSV schema and prompt version. Measure attempts, rejection reasons, and cost per approved image. Expand only after the resume and review flow works.

Review the current parameters and price on the Seedream 5.0 Pro API page, then run one product before funding a full catalog batch. For product references rather than text-only generation, open the Seedream 5.0 Pro Image Edit API.

Bring Your Ideas to Life

Turn a simple prompt or reference into polished AI images and videos in seconds—no setup required.

Start creating
Bring Your Ideas to Life
Related models
All models
Bytedance
10% OFF
MiniMax
30% OFF
DeepSeek
OpenAI
5% OFF

FAQ

How do I create multiple product images with the Seedream 5.0 Pro API?

Create one request per SKU. Read product variables and reference URLs from a CSV, run a small number of requests concurrently, use filenames tied to SKUs, and record each result in a manifest. The script above implements that pattern.

Can Seedream 5.0 Pro generate multiple images at once?

Not as one request returning a catalog of separate images. The current Pro model does not expose text-to-multiple-images. You can still generate multiple images in Seedream 5 Pro by running independent requests concurrently through a controlled queue.

How many reference images can I use with Seedream 5.0 Pro on GPTProto?

The current image-edit endpoint accepts up to 10 references. The first is included in the base price, and each additional reference adds $0.0027. Give each reference one explicit role in the prompt.

How much does a Seedream 5.0 Pro API batch cost?

At the price checked on September 17, 2026, a 1K image costs $0.0405 and a 2K image costs $0.081. Add $0.0027 for every reference after the first, then multiply by the average number of attempts required for one approved result. Check the live model page before budgeting a production run.

Is Seedream 5.0 Pro suitable for a business or team workflow?

Yes, when generation is paired with stable SKU mapping, prompt versions, a resumable manifest, protected API keys, named reviewers, and approval states. The API creates draft assets. Your workflow decides which assets can be published.

Should I generate product images at 1K or 2K?

Use 1K to test the shared prompt across representative SKUs. Once the prompt is stable, use the resolution required by the final placement. Treat every 2K request as a new generation and review it again rather than assuming it will match the 1K composition.

Related Articles

More Blogs
5 Best Affordable AI Video APIs in 2026: Pricing, Ecommerce, and Short Drama

5 Best Affordable AI Video APIs in 2026: Pricing, Ecommerce, and Short Drama

Vidu Q3 Turbo is the best affordable AI video API for most developers in 2026. A five-second 720p clip costs about $0.24, while 1080p costs $0.056 per generated second. Seedance 2.0 Mini is better for cheap drafts, Hailuo 2.3 Standard for fixed six-second action clips, Kling 3.0 Standard for dialogue, and Wan 3.0 for longer multi-shot stories. Those winners change when you add resolution, minimum clip length, audio, and failed attempts. This comparison looks beyond the lowest advertised rate to estimate what each API costs for the shot you can actually use. Price note: GPTProto prices in this guide were checked on September 15, 2026. Video API rates and available settings can change, so confirm the live model page before budgeting a production run. Try Vidu Q3 Turbo

Tiffany Layne | 2026-09-16

6 Best Affordable LLM APIs for AI Agents in 2026

6 Best Affordable LLM APIs for AI Agents in 2026

An affordable LLM API for an AI agent is not necessarily the model with the lowest input-token price. An agent may choose a tool, construct arguments, read the result, revise its plan, and call another tool before it produces a useful answer. A cheap model that makes invalid calls or needs several retries can therefore cost more than a slightly more expensive model that finishes the task once. This guide compares six agent-ready models available through GPTProto. The ranking considers API price, tool use, independent performance evidence, speed, context limits, and the practical risk of paying for unnecessary agent loops. It is a public-benchmark and pricing comparison—not a claim that we ran a private head-to-head test. One Key for Your Team Quick answer: GLM-5.3 Flash is the strongest default for most cost-sensitive agents. DeepSeek Flash is the faster open-weight alternative, while GPT-5.6 Luna is promising for lightweight, high-volume work once its live route price is confirmed. MiniMax M3 fits long document sessions, Gemini 3.8 Flash leads on multimodal speed, and Grok 4.6 is better treated as an escalation model for harder tasks.

Michael Johnson | 2026-09-15

How to Make AI Live Wallpaper with Midjourney and Seedance 2.5

How to Make AI Live Wallpaper with Midjourney and Seedance 2.5

You can follow the same process without writing code: create or choose an image, ask an image-capable chat model for a motion prompt, animate the image, and download the result. The final step depends on your device. Windows and Android can use video wallpaper apps; iPhone needs a compatible Live Photo for its animated Lock Screen.

Tiffany Layne | 2026-09-09

Generate Multiple Images at Once in ChatGPT

Generate Multiple Images at Once in ChatGPT

TL;DR Mastering how to generate multiple images at once in chatgpt involves a combination of structured prompting in the chat UI and using specific parameters or loops via the OpenAI API. While the standard interface defaults to single outputs, you can bypass this bottleneck with grid layouts and batch commands. Most users struggle because the web interface is designed for conversational simplicity, not high-volume production. By understanding the underlying mechanics of DALL-E 3, you can start treating the tool like a creative factory rather than a simple chatbot. This guide explores the transition from manual one-off requests to automated or structured batch workflows, ensuring you never have to wait for a single image to render before starting the next creative concept.

Schuyler Stacy | 2026-08-31