# Bulk OG images from a CSV or a Google Sheet

> Generate hundreds of Open Graph or social images from one template and a spreadsheet: CSV or Google Sheets in, a ZIP and image URLs out. API and dashboard.

Published 2026-10-24 · https://shareglint.com/en/guides/bulk-og-images-from-csv-google-sheets

Most social images are not one-offs. An online shop needs a card for every product, a blog for every post in the archive, a conference for every speaker, a job board for every opening. The design is the same each time; only the words and the photo change. That is exactly what a spreadsheet is good at — one row per item, one column per field — and exactly what **bulk generation** turns into images: one template, a list of rows, an image for every row.

This guide shows how to go from a CSV file or a Google Sheet to a folder of finished Open Graph images, with the API or without writing code, and how to avoid the problems that usually make bulk runs painful: wrong column names, broken encodings, private sheets, and paying twice for the same picture.

## The model: a template plus rows

A template is the fixed part — layout, fonts, colours — with named fields for the parts that change: `title`, `author`, `price`, `photo`. A row fills those fields. If your template has the fields `title` and `author`, a spreadsheet for it looks like this:

| title | author | id | filename |
|---|---|---|---|
| Ten things that ruin your sleep | Anna Kowalska | post-1 | ten-things |
| Why we moved to Postgres | Maya Chen | post-2 | postgres |

Two extra columns are worth adding. `id` is your own identifier — a product SKU, a post slug — and comes back with each row's result, so you can match images to your records without relying on row order. `filename` names the file in the ZIP; without it, files are named after `id`, and without that, after the row number.

Columns are matched to fields **by name**, ignoring case, spaces and punctuation: `Title`, ` title ` and `TITLE` all fill `title`, and `Author Name` fills `author_name`. Columns that match no field — notes, publishing dates, internal flags — are simply ignored, so you can point the tool at the sheet you already have instead of building a copy for it.

## Getting the CSV right

CSV looks simple and is not. [RFC 4180](https://www.rfc-editor.org/rfc/rfc4180) describes the common format — comma-separated, fields with commas or line breaks wrapped in double quotes, a quote inside a field doubled — but real files differ from it in a few predictable ways, and a good importer handles all of them:

- **Semicolons.** Excel in Poland, Germany, France and much of Europe writes `;` instead of `,`, because the comma is the decimal separator there. The separator should be detected, not assumed.
- **The byte-order mark.** Excel's "CSV UTF-8" puts three invisible bytes at the start of the file. Without stripping them, the first column's name starts with an invisible character and never matches `title`.
- **Older encodings.** A file saved as plain "CSV" by an old Excel is in Windows-1252, so `Café` arrives as broken bytes unless the importer falls back to that encoding.
- **Empty lines and empty cells.** A trailing blank line is not a row. An empty cell should leave the field to the template's default rather than print nothing.

Shareglint handles each of these, so the practical advice is short: keep a header row, one row per image, and export with whatever your spreadsheet offers.

## Using a Google Sheet directly

You do not need to download the sheet. Google Sheets can serve any tab as CSV, and a bulk tool can read it from there — as long as the sheet is readable without signing in. Two ways to allow that, both explained in Google's help on [publishing and sharing files](https://support.google.com/docs/answer/183965):

1. **Share → General access → "Anyone with the link" (Viewer).** Then paste the address from your browser bar, for example `https://docs.google.com/spreadsheets/d/1AbC…/edit#gid=0`. The `gid` picks the tab.
2. **File → Share → Publish to web.** Publishing creates a separate public address; it is the better choice when the sheet holds other data you do not want to share in edit view.

A private sheet answers with Google's sign-in page instead of data. Shareglint recognises that and stops with `batch_sheet_not_shared` and the steps above, instead of rendering a batch of images that say `<!DOCTYPE html>`.

## Doing it with the API

One request creates the whole batch. Pass the template, then exactly one source of rows: `rows` as a JSON array, `csv` with the text of a file, or `csv_url` with a CSV address or a sheet link.

```bash
curl -X POST "https://shareglint.com/v1/batches" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "blog-og",
    "sizes": ["og", "square"],
    "format": "webp",
    "csv_url": "https://docs.google.com/spreadsheets/d/1AbC…/edit#gid=0",
    "webhook_url": "https://example.com/hooks/images"
  }'
```

The answer comes at once — `202` with the batch `id` — and the images render in the background. `sizes` renders each row in several formats from the same design (here the 1200×630 Open Graph card and a 1080×1080 square for Instagram), one image per row and size. To map columns yourself, add `columns`, for example `{"title": "Headline", "filename": "Slug"}`.

Follow progress with `GET /v1/batches/{id}`: the status, counts of finished and failed images, a 0–100 `progress`, and every row with its image URL or the reason it failed. When the last row is done, `GET /v1/batches/{id}/zip` redirects to a ZIP of all the images, and the `webhook_url` receives one `batch.completed` event carrying the ZIP's address — one event for the batch, not one per row.

The same request is available to AI assistants through the [MCP server](https://shareglint.com/en/guides/og-images-with-ai-assistants-mcp) as `create_batch` and `get_batch`, so "make cards for every product in this sheet" works from Claude or Cursor.

## Doing it without code

The dashboard has the same flow under **Bulk**: pick a template and sizes, upload a CSV or paste a sheet link, and check the column matching it proposes. Then preview the first rows — previews are free and do not touch your quota — start the batch, and download the ZIP when the progress bar fills. Past batches stay listed with their ZIPs for as long as your images are kept.

## What it costs, and what it does not

Every image counts as one render, exactly like a single API call, and the rules that make single renders cheap apply to batches too:

- **The whole batch is checked first.** If it needs more images than you have left this month (top-up packs included), nothing starts and you get `402` with how many images are needed and how many are available — rather than half a batch and a surprise.
- **Images are charged as they render.** A row whose render fails is refunded automatically.
- **Duplicates are free.** A row identical to an image you already rendered — through the API, a signed URL, or an earlier batch — reuses that image. Repeated rows within one batch render once.

Rows per batch depend on the plan: Free 50, Starter 500, Pro 1,000, Scale 2,000, Business 5,000. A batch runs on its own low-priority queue, a few images at a time per account, so a thousand-row import never slows down the images your site requests live.

## How this compares

Template image services such as [Bannerbear](https://www.bannerbear.com/) and [Placid](https://placid.app/) have offered spreadsheet-driven generation for years, mostly inside their editors and no-code integrations. If your designers live in those editors, that is a good reason to stay. The difference here is that the template can be your own HTML and CSS, the whole run is one API call that also accepts a sheet link, and you pay per image at Shareglint's prices — $4.50 per 1,000 on Starter — with rows already rendered costing nothing.

## FAQ

### How do I generate many images from one template at once?

Put one row per image in a spreadsheet, with a column for each template field, and send it to a bulk endpoint. With Shareglint that is POST /v1/batches with the rows as JSON, a CSV file, or a Google Sheets link; every row becomes one image, and you get each image's URL plus a ZIP of all of them.

### Can I use a Google Sheet directly, without exporting a CSV?

Yes, if the sheet is shared with "Anyone with the link" or published to the web. Paste the address from the browser bar; the tab in the #gid part of the address is fetched as CSV. A private sheet cannot be read, and the API answers with a clear error instead of rendering a login page.

### What does a bulk run cost?

Each image counts as one render against your monthly quota, the same as a single API call. Rows identical to an image you already have are free, a row that fails is refunded, and the whole batch is checked against your remaining quota before anything starts.

### How many rows can one batch have?

It depends on the plan: Free 50, Starter 500, Pro 1,000, Scale 2,000, Business 5,000. Larger lists can be split into several batches; they run one after another on your account.
