Auto-Generate Product Listing Images with Price Badges and Branding

7 min read Image Generation

The Problem with Product Images at Scale

You have 500 products. Each one needs a listing image for your storefront — product photo, price tag, brand logo, maybe a “SALE” badge when it’s on discount. Your designer creates a beautiful Figma template. Then someone has to manually export 500 images. Every price change means re-exporting. Every new product means opening Figma again.

Some teams try to solve this with HTML/CSS rendered in a headless browser. That works until you need pixel-perfect typography, consistent cross-platform rendering, and sub-second generation times. Headless Chrome is slow, memory-hungry, and notoriously inconsistent with font rendering across environments.

The Image Generation API generates product listing images from a JSON template. Define your layout once — product photo, price badge, brand elements — and generate thousands of variants by swapping the dynamic data.

Anatomy of a Product Listing Image

A typical product listing image has five visual elements:

  • A background — solid color or subtle gradient
  • The product photo — cropped to fit consistently
  • A price badge — colored rectangle with price text
  • A brand watermark — logo in the corner
  • Optional status text — “SALE”, “NEW”, “SOLD OUT”

Each of these maps directly to a layer in the API. Layers render in index order — 0 at the bottom, last on top. You stack them like you would in any design tool.

The Template

Here’s a complete listing image template for a 1080x1080 product card:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const result = await client.generateImage({
  dimensions: { width: 1080, height: 1080 },
  output_format: "png",
  fonts: [
    {
      name: "Inter",
      weight: "Bold",
      style: "normal",
      file: { type: "url", name: "Inter-Bold.woff2", url: "https://cdn.example.com/fonts/Inter-Bold.woff2" },
    },
    {
      name: "Inter",
      weight: "Regular",
      style: "normal",
      file: { type: "url", name: "Inter-Regular.woff2", url: "https://cdn.example.com/fonts/Inter-Regular.woff2" },
    },
  ],
  layers: [
    {
      index: 0,
      type: "solid-color-background",
      hex_color: "#F8F8F8",
    },
    {
      index: 1,
      type: "static-image",
      file: { type: "url", name: "sneaker-white.jpg", url: "https://cdn.example.com/products/sneaker-white.jpg" },
      position: { x: 40, y: 40 },
      dimensions: { width: 1000, height: 800 },
      should_use_smart_cropping: true,
    },
    {
      index: 2,
      type: "static-image",
      file: { type: "url", name: "logo.png", url: "https://cdn.example.com/brand/logo.png" },
      position: { x: 40, y: 960 },
      dimensions: { width: 120, height: 80 },
      opacity: 60,
    },
    {
      index: 3,
      type: "rectangle",
      position: { x: 740, y: 860 },
      dimensions: { width: 300, height: 80 },
      hex_color: "#1A1A1A",
    },
    {
      index: 4,
      type: "text",
      text: "**$129.99**",
      position: { x: 740, y: 860 },
      dimensions: { width: 300, height: 80 },
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 32,
      text_color: "#FFFFFF",
      text_align: "center",
      vertical_align: "center",
    },
  ],
});

const { data: { buffer: imageBase64 } } = result;
const imageBuffer = Buffer.from(imageBase64, "base64");

Let’s break down what each layer does.

Layer 0 is a light gray background. Solid color, nothing else. This gives every listing image a consistent base regardless of the product photo’s background.

Layer 1 is the product photo. The should_use_smart_cropping flag is the key detail here — it uses AI object detection to find the product in the photo and crop around it. If the photo is a sneaker shot against a studio backdrop with lots of dead space, smart crop centers the sneaker in the 1000x800 container. No manual positioning per product.

Layer 2 is the brand logo, semi-transparent in the bottom-left corner. The opacity: 60 keeps it visible without competing with the product.

Layer 3 is a dark rectangle — the price badge background.

Layer 4 is the price text, positioned to fill the same rectangle. The text_align: "center" and vertical_align: "center" center the price within the badge. The **$129.99** uses markdown bold syntax for the heavier weight.

Adding a Sale Badge

When a product is on sale, you want a visual indicator — a colored badge in the top corner. Add two more layers:

// Insert these after the product photo layer
{
  index: 2,
  type: "rectangle",
  position: { x: 840, y: 40 },
  dimensions: { width: 200, height: 60 },
  hex_color: "#E53E3E",
},
{
  index: 3,
  type: "text",
  text: "**SALE**",
  position: { x: 840, y: 40 },
  dimensions: { width: 200, height: 60 },
  font_name: "Inter",
  font_weight: "Bold",
  font_size_in_px: 28,
  text_color: "#FFFFFF",
  text_align: "center",
  vertical_align: "center",
},

Red rectangle, white text. Same pattern as the price badge — a rectangle layer for the background, a text layer on top for the label.

You can swap “SALE” for “NEW”, “SOLD OUT”, “LIMITED” — whatever status applies. Change the rectangle hex_color to match: red for sale, green for new, gray for sold out.

Batch Generation from a Product Database

The real value shows up when you generate images for your entire catalog. The template stays the same. Only the product-specific data changes — the photo URL, the price, and optionally the sale badge.

const products = [
  { name: "White Sneaker", photoUrl: "https://cdn.example.com/products/sneaker-white.jpg", price: "$129.99", isOnSale: false },
  { name: "Running Shoe", photoUrl: "https://cdn.example.com/products/running-shoe.jpg", price: "$89.99", isOnSale: true },
  { name: "Leather Boot", photoUrl: "https://cdn.example.com/products/leather-boot.jpg", price: "$199.99", isOnSale: false },
  // ... hundreds more from your database
];

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const generateListingImage = async (product) => {
  const baseLayers = [
    {
      index: 0,
      type: "solid-color-background",
      hex_color: "#F8F8F8",
    },
    {
      index: 1,
      type: "static-image",
      file: { type: "url", name: "product.jpg", url: product.photoUrl },
      position: { x: 40, y: 40 },
      dimensions: { width: 1000, height: 800 },
      should_use_smart_cropping: true,
    },
    {
      index: 2,
      type: "static-image",
      file: { type: "url", name: "logo.png", url: "https://cdn.example.com/brand/logo.png" },
      position: { x: 40, y: 960 },
      dimensions: { width: 120, height: 80 },
      opacity: 60,
    },
    {
      index: 3,
      type: "rectangle",
      position: { x: 740, y: 860 },
      dimensions: { width: 300, height: 80 },
      hex_color: "#1A1A1A",
    },
    {
      index: 4,
      type: "text",
      text: `**${product.price}**`,
      position: { x: 740, y: 860 },
      dimensions: { width: 300, height: 80 },
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 32,
      text_color: "#FFFFFF",
      text_align: "center",
      vertical_align: "center",
    },
  ];

  const saleBadgeLayers = product.isOnSale
    ? [
        {
          index: 5,
          type: "rectangle",
          position: { x: 840, y: 40 },
          dimensions: { width: 200, height: 60 },
          hex_color: "#E53E3E",
        },
        {
          index: 6,
          type: "text",
          text: "**SALE**",
          position: { x: 840, y: 40 },
          dimensions: { width: 200, height: 60 },
          font_name: "Inter",
          font_weight: "Bold",
          font_size_in_px: 28,
          text_color: "#FFFFFF",
          text_align: "center",
          vertical_align: "center",
        },
      ]
    : [];

  const result = await client.generateImage({
    dimensions: { width: 1080, height: 1080 },
    output_format: "png",
    fonts: [
      {
        name: "Inter",
        weight: "Bold",
        style: "normal",
        file: { type: "url", name: "Inter-Bold.woff2", url: "https://cdn.example.com/fonts/Inter-Bold.woff2" },
      },
    ],
    layers: [...baseLayers, ...saleBadgeLayers],
  });

  const { data: { buffer } } = result;

  return { name: product.name, buffer: Buffer.from(buffer, "base64") };
};

// Generate all listing images in parallel batches
const batchSize = 10;
for (let i = 0; i < products.length; i += batchSize) {
  const batch = products.slice(i, i + batchSize);
  const results = await Promise.all(batch.map(generateListingImage));
  // Upload results to your CDN
}

The loop processes products in batches of 10. Each batch runs in parallel. For 500 products, that’s 50 sequential batches — the whole catalog in minutes instead of hours of manual work.

Why Smart Crop Matters for Product Photos

Product photos come in every aspect ratio and composition. A sneaker might be centered in the frame. A handbag might be off to one side with negative space. A piece of jewelry might fill a tiny portion of a large studio shot.

Without smart crop, you’d need to manually specify crop coordinates per product — or accept that center-cropping would cut off half your products. The should_use_smart_cropping flag handles this automatically. It detects the product in the frame and positions it within the container dimensions you specified.

This is the difference between a listing page where every product is consistently framed and one where products are randomly cropped.

Handling Multiple Formats

Different marketplaces and channels need different image specifications. Amazon wants different dimensions than Shopify. Social media ads need different aspect ratios than product pages.

Same template, different dimensions:

const formatSpecs = [
  { name: "storefront", width: 1080, height: 1080 },
  { name: "marketplace", width: 1600, height: 1200 },
  { name: "social-ad", width: 1200, height: 628 },
];

Adjust the layer positions proportionally for each format, or build separate templates per channel. The generation cost is the same regardless of dimensions.

Updating Prices in Real Time

When prices change — a flash sale, seasonal discount, price adjustment — you regenerate the affected images. No designer needed. No Figma template to open. Your backend updates the price in the database and triggers a regeneration.

The same applies to branding changes. New logo? Update the URL in the template. New brand colors? Change the hex codes. Every image in the catalog gets the update on the next generation run.

Get Started

Check the docs for the full layer reference and output format options. The TypeScript and Python SDKs handle authentication and response parsing.

Sign up for a free account — no credit card required. Start with one product image and scale to your full catalog when you’re ready.

Start building in minutes

Free trial included. No credit card required.