Menu
Sign up

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 November 6, 2026 · 5 min read

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.

// 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, 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). 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), so a custom function can build the URL inside the sheet:

/**
 * 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. 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) — 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 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).

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.