# Generate images from Google Sheets with Apps Script

> Turn Google Sheets rows into images: an Apps Script menu that renders each row and shows it with =IMAGE, signed URLs as a formula, and Google's quotas.

Published 2026-11-06 · https://shareglint.com/en/integrations/google-sheets

To generate images from Google Sheets, add a short **Apps Script** to the spreadsheet: a menu item reads each row, calls `https://shareglint.com/v1/images?wait=true` with `UrlFetchApp`, writes the returned image URL into the row, and puts `=IMAGE(…)` next to it so the picture shows in the cell. For a whole sheet at once there is also the batch API, which takes the published sheet as a CSV. No add-on is involved; everything below runs in your own spreadsheet.

## Option 1: a "Render images" menu

The sheet: column A `title`, column B `author`, column C the image URL (filled by the script), column D the preview. Open **Extensions → Apps Script**, paste the script, and save your API key under **Project Settings → Script properties** as `API_KEY` — not in a cell, which everyone the sheet is shared with can read.

```javascript
// Extensions → Apps Script. Adds Images → Render new rows.
function onOpen() {
  SpreadsheetApp.getUi().createMenu('Images')
    .addItem('Render new rows', 'renderNewRows')
    .addToUi();
}

function renderNewRows() {
  const key = PropertiesService.getScriptProperties().getProperty('API_KEY');
  const sheet = SpreadsheetApp.getActiveSheet();
  const last = sheet.getLastRow();
  if (last < 2) return;

  const rows = sheet.getRange(2, 1, last - 1, 3).getValues();
  rows.forEach(([title, author, url], i) => {
    if (!title || url) return; // skip empty rows and rows already rendered
    const res = UrlFetchApp.fetch('https://shareglint.com/v1/images?wait=true', {
      method: 'post',
      contentType: 'application/json',
      headers: { Authorization: 'Bearer ' + key },
      payload: JSON.stringify({ template: 'blog-og', modifications: { title: String(title), author: String(author) } }),
      muteHttpExceptions: true,
    });
    const body = JSON.parse(res.getContentText());
    const row = i + 2;
    if (res.getResponseCode() !== 200) {
      // The error goes into the preview column, so the row is retried next time.
      sheet.getRange(row, 4).setValue('Error ' + res.getResponseCode() + ': ' + (body.error || ''));
      return;
    }
    sheet.getRange(row, 3).setValue(body.url);
    sheet.getRange(row, 4).setFormula('=IMAGE(C' + row + ')');
  });
}
```

Reload the sheet, choose **Images → Render new rows**, and allow the script to connect to an external service the first time. Each row gets its image URL in column C and a preview in column D. Rows that already have a URL are skipped, so the menu can be run again after adding rows; clear a URL to render that row anew. The same values again return the existing image without using quota, so a run that stops halfway and is restarted costs nothing extra.

Error codes end up in the preview column, and the row stays without a URL so the next run tries it again: `402` means the month's renders are used up, `422` an invalid request (the `error` code says which), `503` that the image was not ready within the wait — run the menu again and it is picked up from the cache.

**Google's limits** ([Apps Script quotas](https://developers.google.com/apps-script/guides/services/quotas), last updated 2026-09-03): a script run stops after **6 minutes**, and URL Fetch calls are limited to **20,000 a day** on a consumer account and 100,000 on Google Workspace, per user. A still render usually answers within a few seconds, so one menu run handles dozens to a few hundred rows; for more, use option 3.

**Showing the image.** `=IMAGE(url, mode)` inserts the image into the cell; mode 1 fits it inside the cell keeping the aspect ratio, 3 keeps the original size, 4 takes a custom height and width ([Google Docs Editors Help](https://support.google.com/docs/answer/3093333)). The render URL is a public PNG or JPEG address, which is what `IMAGE` needs.

## Option 2: a signed URL as a formula

A signed image URL is computed from your account id and signing secret, with no request to the API: the image renders the first time something fetches the URL. Apps Script has HMAC-SHA256 built in (`Utilities.computeHmacSha256Signature`, which returns a byte array — [reference](https://developers.google.com/apps-script/reference/utilities/utilities)), so a custom function can build the URL inside the sheet:

```javascript
/**
 * Signed image URL: =SIGNED_IMAGE_URL("blog-og", "title", A2, "author", B2)
 * @customfunction
 */
function SIGNED_IMAGE_URL(template, ...pairs) {
  const props = PropertiesService.getScriptProperties();
  const account = props.getProperty('ACCOUNT_ID');
  const secret = props.getProperty('SIGNING_SECRET');
  // RFC 3986: encodeURIComponent leaves ! ' ( ) * alone.
  const enc = (v) => encodeURIComponent(String(v))
    .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());

  const fields = {};
  for (let i = 0; i + 1 < pairs.length; i += 2) fields[pairs[i]] = pairs[i + 1];

  const path = '/i/' + account + '/' + template + '.png';
  const query = Object.keys(fields).sort().map((k) => enc(k) + '=' + enc(fields[k])).join('&');
  const bytes = Utilities.computeHmacSha256Signature(path + '?' + query, secret);
  const hex = bytes.map((b) => ((b + 256) % 256).toString(16).padStart(2, '0')).join('');
  return 'https://shareglint.com' + path + '?' + query + '&s=' + hex.slice(0, 32);
}
```

The canonical string is the path, `?`, and every parameter except `s` sorted by name and RFC 3986-encoded; `s` is the first 32 hex characters of the HMAC — the same algorithm as in the [docs](https://shareglint.com/en/docs#signed). Apps Script returns the HMAC as signed bytes (−128 to 127), hence the `+ 256`.

Use the formula where the URL is consumed outside the sheet: a column your CMS imports as `og:image`, a merge field for an email campaign, a link list for a partner. The signing secret should be in Script properties, as above; anyone who can edit the script can read it, so keep editing rights to people who may sign URLs. Custom functions must return within 30 seconds ([custom functions](https://developers.google.com/apps-script/guides/sheets/functions)) — computing a signature takes milliseconds. Dates and numbers are signed as their text (`String(value)`); wrap a date in `TEXT(A2, "yyyy-mm-dd")` to control the format. (Sheets in some locales separate arguments with `;` instead of `,`.)

## Option 3: the whole sheet as one batch

For hundreds or thousands of rows, skip the script: publish the sheet (or a tab) as CSV and send its link to `POST /v1/batches` with `csv_url`. Column headers become field names, `id` and `filename` are kept for the results, and one ZIP holds every image. Rows per batch by plan: Free 50, Starter 500, Pro 1,000, Scale 2,000, Business 5,000. The [CSV and Google Sheets guide](https://shareglint.com/en/guides/bulk-og-images-from-csv-google-sheets) walks through publishing the sheet and the columns.

## Cost

One render per image and size; repeats with the same values are free. The Free plan's 100 renders a month carry a watermark; Starter is $9 a month for 2,000 renders ([pricing](https://shareglint.com/en/pricing)).

## What it does not do

- It is not a Google Workspace add-on: there is nothing to install from the Marketplace, only the script in your sheet.
- It does not read the sheet by itself; the script sends only the columns it is written to send.
- It does not keep the images in Drive — the URL points to our file, kept for your plan's retention period. A Drive copy needs one more line in the script (`DriveApp.createFile(UrlFetchApp.fetch(url).getBlob())`).

## FAQ

### Can Google Sheets generate images from rows?

Yes, with a short Apps Script: a menu item sends each row to the image API with UrlFetchApp, writes the returned image URL into the row, and an =IMAGE formula shows it in the cell. No add-on to install.

### Can I make the image with a formula instead of a script run?

You can compute a signed image URL with a custom function, since Apps Script has HMAC-SHA256 built in. The URL renders the image the first time anything fetches it, which suits og:image tags and emails.

### How many rows can I render?

An Apps Script run stops after 6 minutes, and URL Fetch calls are limited to 20,000 a day for a consumer account (100,000 on Google Workspace). For hundreds of rows at once, send the sheet as one batch instead.

### Where should the API key go?

In the script's Script properties, not in a cell, because anyone the sheet is shared with can read cells. Editors of the script can still read its properties, so use a key made only for this sheet.
