Guide · 2026-10-11

Images from a CSV

One template, one spreadsheet, one request. Every row becomes an image at a hosted URL, and the columns decide what changes.

The shape of the CSV

The header row names the columns, and each name decides what the column does:

  • A placeholder name like title fills {{title}} in the template.
  • layer.property like photo.src or accent.fill sets that property on the layer with that name. Numbers, true/false and colours are read for the properties that take them, and a bad value fails the request with its row and column before anything is charged.
  • A leading underscore like _sku keeps the cell as metadata on that row's image, so you can match images back to your rows.

This CSV fits the product review starter, whose placeholders are title, reviews and photo, and whose layers include stars (a rating) and bar:

title,reviews,photo,stars.value,bar.value,_sku
The best headless image API we have tested,1284,https://example.com/a.jpg,4.5,90,A-100
"Queues, retries and idempotency",312,https://example.com/b.jpg,4,80,A-101
Postgres at 40 TB,97,https://example.com/c.jpg,,100,A-102

The empty stars.value in the last row keeps the template's own rating; an empty placeholder column renders empty. Quote any cell that contains the delimiter. Commas, semicolons, tabs and pipes are all detected from the header.

Post it

Save a template once (or copy a starter into your account), then post the file with the template's id in the query string:

curl -X POST "https://api.imageapis.com/v1/collections?template=tpl_…&scale=2" \
  -H 'X-API-Key: YOUR_KEY' -H 'Content-Type: text/csv' --data-binary @products.csv

Or send JSON, which is what an agent over MCP does with create_collection:

{ "template": "tpl_…", "csv": "title,author\nOne,Dana\nTwo,Sam\n", "variables": { "site": "example.com" } }

variables at the top level are defaults for every row; a row's own columns win.

The answer is 202 right away, with every image's id and final URL already assigned:

{
  "id": "col_5kq2wz8xmtnb4hdc7vre", "status": "pending", "total": 3, "units": 3,
  "csv": { "rows": 3, "delimiter": "comma", "variables": ["title", "reviews", "photo"],
           "modifications": ["stars.value", "bar.value"], "metadata": ["_sku"] },
  "warnings": [],
  "images": [
    { "index": 0, "id": "img_…", "status": "pending", "url": "https://img.imageapis.com/i/img_….png", "metadata": { "sku": "A-100" } }
  ]
}

Read csv to check that each column was understood the way you meant. warnings lists columns that fill nothing and placeholders nothing fills, which is how a typo like titel shows up.

Wait for it, in Python

import os, time, requests

API = "https://api.imageapis.com"
H = {"X-API-Key": os.environ["IMAGEAPIS_KEY"]}

with open("products.csv", "rb") as f:
    col = requests.post(f"{API}/v1/collections", params={"template": "tpl_…"},
                        headers={**H, "Content-Type": "text/csv"}, data=f).json()

while col["status"] in ("pending", "running"):
    time.sleep(2)
    col = requests.get(f"{API}/v1/collections/{col['id']}", headers=H).json()

for img in col["images"]:
    print(img["metadata"].get("sku"), img["status"], img["url"])

Rows render five at a time, so a thousand rows take a few minutes. For anything that size, pass webhook_url instead of polling. The webhook gets one POST with the whole collection when the last row is done, with the event collection.completed, or collection.failed if every row failed.

Straight from Google Sheets

A sheet shared as "anyone with the link can view" is already a CSV at its export URL, so there is nothing to download by hand:

sheet = "https://docs.google.com/spreadsheets/d/<sheet id>/export?format=csv&gid=0"
csv = requests.get(sheet).text
col = requests.post(f"{API}/v1/collections", headers=H, json={"template": "tpl_…", "csv": csv}).json()

Name the sheet's header cells after the template's placeholders and layers, and the sheet becomes the control panel for the images.

Cost, limits and failed rows

  • Each row is one image, the same as POST /v1/images. Reading the collection is free.
  • The whole collection is reserved before anything renders. If the plan has fewer images left than there are rows, the answer is 429 with needed and remaining, and nothing is rendered or charged.
  • A collection takes up to 1,000 rows and a 2 MB body. Split bigger files across calls.
  • A row that fails, for example an image URL that does not load, is marked failed with the reason, and the rest carry on. GET /v1/collections/<id>?status=failed lists only those rows, with their metadata, so you can fix them and post them again as a smaller CSV. Rows that failed still count as images.
  • Images are kept 30 days on the free plan and a year on paid plans, like every render.

The free CSV to images tool runs the same thing on up to 10 rows without a key, and shows the request for your file.

Get a free key: 150 images a month API reference