Batch Image Generation: 50 Jobs at a Time

Published October 15, 2026 by Balaji

Uncategorized

Thousands of images means loops of 50. POST /api/v1/renders/batch accepts at most 50 jobs. This tutorial covers pacing, webhooks, retries, and why a naive 5,000-long HTTP array will 400.

Overview

Generating one image with an API is easy. Generating ten thousand is a different problem entirely — and it’s the problem every growing product eventually hits: a catalog with thousands of SKUs that each need a banner, a blog with hundreds of posts that each need an Open Graph card, an event platform that issues a unique ticket per registration, a course platform that mints a certificate per completion.

Doing that with a design tool is impossible. Doing it with a headless browser is a maintenance sink. Doing it with an image generation API means learning how to batch — how to design one template, feed it thousands of data rows, and get thousands of images back without babysitting the process.

This tutorial covers exactly that: the mechanics of batch generation with jsontoimg, when to use synchronous vs webhook delivery, how to structure your data for a clean batch, the cost math that makes batches worth it, and the failure modes that will bite you at volume. You’ll finish with a working Node.js script that turns a spreadsheet of rows into a folder of finished images.

Why Batch Generation Is a Different Problem

The naive approach is a loop: for each row of data, call the API, wait, save the image. It works — up to a point. At 100 images it’s tolerable. At 5,000 images, three things break:

  1. Latency compounds. If one render takes 1–2 seconds, 5,000 sequential requests is 1.5–3 hours of waiting, most of it idle time on your side.
  2. Rate limits. Every API has throughput limits. A naive loop slams the endpoint with no pacing and trips 429s in the first few minutes.
  3. Failures pile up. At volume, individual requests fail. Without retry logic, error tracking, and idempotency, you end up with 4,783 images and no idea which 217 are missing.

Batch generation solves all three at once. Instead of N requests, you send one request containing N data objects, the API handles the parallelism and pacing internally, and you get an array of results back — or a webhook when they’re done.

This is the same pattern the whole category settled on: Bannerbear’s Multi Image API renders several images per request and delivers results via webhook (Bannerbear API docs), Placid offers both synchronous and webhook delivery (Placid REST API docs), and RenderForm describes bulk generation as “a single template, data for the images, and multiple images generated at once” (RenderForm bulk image generation). jsontoimg follows the same conventions, so the mental model transfers.

Step 1: Understand the Batch Model

With jsontoimg, a batch is simple: you send an array of data objects against a template, and you get an array of images back.

POST /api/v1/renders/batch

{
  "jobs": [
    {
      "template": "product-banner-v1",
      "format": "png",
      "layers": {
        "product_name": { "text": "Wireless Mouse" },
        "price": { "text": "$29" }
      }
    },
    {
      "template": "product-banner-v1",
      "format": "png",
      "layers": {
        "product_name": { "text": "Mechanical Keyboard" },
        "price": { "text": "$89" }
      }
    }
  ]
}

Each object fills the placeholders in the template — same design, different data, thousands of times. The key insight: your data IS the design. Every variable — text, image URL, color, badge, QR code — is a field in JSON. Nothing is hardcoded, so nothing needs a human.

A minimal template for the example above looks like this:

{
  "width": 1200,
  "height": 630,
  "background": { "type": "solid", "color": "#0f172a" },
  "layers": [
    {
      "type": "text",
      "name": "product_name",
      "text": "named layer name",
      "font": { "size": 64, "weight": "bold", "color": "#ffffff" },
      "position": { "x": 80, "y": 200 }
    },
    {
      "type": "text",
      "name": "price",
      "text": "named layer price",
      "font": { "size": 40, "color": "#38bdf8" },
      "position": { "x": 80, "y": 320 }
    }
  ]
}

Placeholders like named layer name are replaced with values from each data object. If you need to design templates from scratch, the Designing Image Templates with JSON guide covers the full layer system.

Step 2: Choose Your Delivery Mode

Batch endpoints generally offer two delivery modes, and choosing correctly matters more than any other decision in this tutorial.

Synchronous (small batches, interactive flows)

You send the batch, the API renders everything, and the response contains all images. Use this when:

  • Batch size is small (tens of images, not thousands)
  • The caller is waiting on the result (e.g., a user uploading a CSV in your dashboard)
  • You want the simplest possible integration

The tradeoff is the request can take a while, and your HTTP client needs a generous timeout. For a 50-image batch at ~1s per render, expect 30–60 seconds.

Webhooks (large batches, background pipelines)

You send the batch with a webhook_url, the API responds immediately with a batch ID, and each completed image — or the whole batch — is POSTed to your URL. Use this when:

  • Batch size is large (hundreds to thousands)
  • Generation happens in the background (nightly catalog refresh, event-triggered jobs)
  • You want automatic retries and a durable audit trail

The pattern is identical to what Bannerbear describes as its primary flow: “POST a request, the API responds immediately with 202 Accepted and you either receive the generated result via webhook or via polling” (Bannerbear async docs). It keeps the request/response cycle predictable no matter how big the job is.

This is the mode the rest of this tutorial uses, because it’s the one that scales.

Step 3: Structure Your Data Like a Pro

Garbage data produces garbage images — and with thousands of images, garbage is expensive to catch. Structure your batch data with these rules:

1. Every row gets a stable ID. Include a name or id field per item. It becomes the output filename and your source of truth for tracking.

{ "id": "A-001", "name": "Wireless Mouse", "price": "$29" }

2. Normalize before you send. Trim whitespace, validate that image URLs resolve, and pre-format currency and dates. An API renders what you give it — it won’t fix “usd 29.0” or a broken image URL.

3. Escape and encode text. JSON handles escaping automatically, but be careful with user-generated content: long strings overflow text layers, newlines break single-line layouts, and emoji can fall back to ugly system fonts. Add a max_chars check per field before sending.

4. Test on a sample batch first. Never run your first batch at full scale (max 50 jobs per request). Generate 10–20 test images, check the output, then scale. The MindStudio guide on batch generation makes exactly this point: “Never run your first batch at full scale (max 50 jobs per request). Generate 10-20 test images to validate your workflow” (MindStudio: Batch AI Image Generation).

Step 4: Build the Batch Script

Here’s a complete Node.js script that reads a CSV of product data and fires a webhook-delivered batch. Install dependencies with npm install csv-parse:

import { parse } from "csv-parse/sync";
import { readFileSync } from "node:fs";

const API_KEY = process.env.JSONTOIMG_API_KEY;
const BATCH_URL = "https://app.jsontoimg.com/api/v1/renders/batch";

// 1. Read and parse your data source (CSV, DB rows, spreadsheet export — anything)
const csv = readFileSync("./products.csv", "utf8");
const rows = parse(csv, { columns: true, skip_empty_lines: true });

// 2. Map rows to template data objects
const data = rows.map((row) => ({
  id: row.sku,
  name: row.product_name.trim(),
  price: `$${Number(row.price).toFixed(0)}`,
  badge: row.badge || "",
}));

// 3. Fire the batch with webhook delivery
const response = await fetch(BATCH_URL, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    jobs: data.slice(0, 50).map((row) => ({
      template: "product-banner-v1",
      format: "png",
      webhookUrl: "https://your-app.com/webhooks/jsontoimg",
      layers: {
        product_name: { text: row.name },
        price: { text: row.price },
        badge: { text: row.badge },
      },
    })),
  }),
});

const batch = await response.json();
console.log(`Batch ${batch.id} accepted ${batch.acceptedCount} jobs`);
console.log(`Poll GET /api/v1/renders/batches/${batch.id}`);

Step 5: Handle the Webhook Results

Your webhook endpoint receives a POST per completed image (or per batch, depending on your setting). The payload should look like:

{
  "id": "batch_8f3k2",
  "status": "processing",
  "acceptedCount": 2,
  "rejectedCount": 0
}

Track results in a database keyed by item_id. This gives you three superpowers:

  • Idempotency — if a webhook is delivered twice, updating the same row is harmless.
  • Retry targeting — after the batch finishes, query for rows where status != "completed" and re-send only those.
  • Auditability — you always know exactly which images exist and which don’t.

Here’s a minimal retry loop that runs after a batch completes:

async function retryFailed(batchId) {
  const failed = await db.query(
    `SELECT item_id FROM batches WHERE batch_id = $1 AND status != 'completed'`,
    [batchId]
  );

  if (failed.length === 0) return console.log("All images generated ✓");

  const rows = failed.map((f) => dataById[f.item_id]);
  await fetch(BATCH_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      jobs: rows.slice(0, 50).map((row) => ({
        template: "product-banner-v1",
        format: "png",
        layers: {
          product_name: { text: row.name },
          price: { text: row.price },
        },
      })),
    }),
  });
  console.log(`Retrying ${rows.length} failed items...`);
}

Step 6: The Cost Math That Justifies the Batch

Batch generation isn’t just convenient — it’s the difference between an automation that makes sense and one that doesn’t. Consider the numbers:

Approach Cost per image Time for 1,000 images Maintenance
Designer in Figma/Canva $5–$50+ (hourly labor) Weeks High (human in loop)
Headless browser (Puppeteer) $0.50–$2.00 (compute + infra) Hours, but you run the infra High (servers, deps, memory leaks)
Template-based API batch Pay-per-render, volume pricing Minutes, fully managed Near zero

The e-commerce comparison is starker. Traditional product photography runs $200–$5,000 per session; AI/template-generated product visuals come in around $0.10–$2.00 per image — for a 500-SKU catalog, that’s a drop from $12,500+ to under $1,000 (Digital Applied: AI product photography costs).

The compounding effect matters too: at 10,000 images, even a $0.01-per-image price difference is $100. That’s why volume pricing and batch-friendly plans exist — and why you should always run a small test batch before committing to a full production run.

One detail worth internalising before the failure list: a 429 is not an error in the sense a 500 is. Per MDN’s reference on 429 Too Many Requests, it is the server telling you the request was well-formed but arrived too soon, and it may carry a Retry-After header saying how long to wait. Treat it as flow control and back off; treat it as a failure and you will drop images that would have rendered fine.

Common Failure Modes at Scale (and How to Avoid Them)

After generating tens of thousands of images, these are the failures that actually happen:

1. Text overflow. A 200-character product name in a 40-char text layer. Fix: enforce max_chars in your data mapping, and design templates with auto_fit or truncation enabled for untrusted fields.

2. Broken image URLs. A logo or product photo that 404s renders as a broken placeholder across thousands of images. Fix: pre-validate all URLs in your batch script with a HEAD request, and fail the batch early if more than X% are broken.

3. Missing retries. A transient network error drops 3% of a 10,000-image batch, and nobody notices for a week. Fix: always track status per item and run the retry loop (Step 5) automatically after every batch.

4. Wrong dimensions for the destination. A 1080×1080 square image uploaded to a 1200×630 OG slot gets cropped or squashed. Fix: define one template per destination format, and name templates by format (og-v1, feed-v1, banner-v1).

5. Cost surprises. A forgotten loop regenerating the same batch nightly burns credits silently. Fix: log batch sizes, set per-batch limits, and alert on abnormal volume.

Beyond Products: The Same Batch Powers Everything

The batch pattern isn’t specific to product banners. The identical flow — template + array of data + webhook — powers every high-volume use case on the site:

  • Open Graph images for every blog post and page (use case)
  • Social media posts scheduled from a spreadsheet (use case)
  • Certificates minted per course completion (use case)
  • Event tickets unique per registration
  • Email hero images personalized per subscriber

Each one is a template, a data source, and a webhook endpoint. If you’ve built the product banner pipeline, you’ve built them all.

Key Takeaways

  • Batch generation is one request with an array of data — the API handles parallelism and pacing, so 5,000 images don’t mean 5,000 requests from your side.
  • Choose delivery mode by size: synchronous for interactive small batches, webhooks for background large batches.
  • Data hygiene is everything — stable IDs, normalized fields, and validated URLs separate a clean 10,000-image run from a disaster.
  • Track per-item status and retry only the failures; idempotent webhook handling makes this trivial.
  • Test at 10–20 images before full scale — the cheapest bug is the one you catch before generating thousands.
  • Template-based API batches cost $0.10–$2.00 per image versus $200+ per traditional shoot session — the cost math is why this category exists.

FAQ

How many jobs can one batch image generation request hold?

Fifty. POST /api/v1/renders/batch accepts at most 50 jobs per request and returns 400 if you send more, so a 5,000-image run is 100 sequential requests of 50 rather than one giant array.

Should I wait for the batch response or use a webhook?

Wait synchronously only for small, interactive batches where a user is watching. For anything large or background, pass a webhook URL and let jsontoimg POST each result as it finishes — that keeps your request from timing out on a long queue.

Do failed jobs in a batch cost credits?

No. One credit is one successful PNG, JPG, WebP, AVIF, or PDF page. A job that errors is not billed, so a partially failed batch only charges for the images you actually received.

How do I avoid rate limits when generating thousands of images?

Send the 50-job batches sequentially rather than firing all of them at once, and retry a 429 with backoff before moving to the next chunk. Pacing the loop is what keeps a bulk run stable.

Is a PDF page billed differently from a PNG?

No. A PDF page costs the same single credit as a still, so a 3-page PDF is 3 credits.

Related Resources

See also batch docs (max 50) and product banners.

Zoom out: How JSON-to-Image Conversion Works maps the whole template, named-layer, and render-job architecture.