Google Sheets can show any image URL with IMAGE(). A RenderScreenshot GET URL returns the image itself, so a website screenshot is one formula. The question is the key. This page adds an Apps Script custom function that builds a signed URL for each cell. Formulas stay short, and the sheet holds no API key. It is part of our integrations hub.
=IMAGE(SCREENSHOT_URL(A2, "og_card", "", RS_REFRESH))
Last tested: 2026-10-18. We ran
Code.gswith Node 22.18 inside a VM context that providesPropertiesServiceand aUtilities.computeHmacSha256Signaturethat returns signed bytes, as Apps Script does. A local stub of our API, which checks signatures with the documented algorithm, accepted all generated URLs and rejected a changed one withinvalid_signature. The same hex conversion gives the documented reference signature. We did not run it inside a Google Sheet for this page, so it has no screenshots of Sheets.
What you can build
- A link audit sheet. Column A holds URLs from a sitemap. Column B shows each page, so broken layouts are visible in one scroll.
- A partner or app directory draft. Each row gets a thumbnail before the data goes to your CMS.
- Text cards from cell values.
HTML_IMAGE_URLrenders a short HTML snippet built withCONCATENATEas an image. Use it for status badges or a quick social card mock-up.
IMAGE() only shows a remote image; the sheet stores no file. To keep a copy of each capture in the record itself, use the Airtable attachment script instead.
Prerequisites
- A RenderScreenshot signing key pair: a public key ID that starts with
rs_pub_and a secret key that starts withrs_secret_. Create a free account, then create the pair on the API Keys page. See API keys. - Edit access to the spreadsheet, to add the script.
Step by step
- Open the sheet and choose Extensions > Apps Script.
- Replace the content of
Code.gswith the script below and save. - Open Project Settings > Script properties. Add
RS_PUBLIC_KEY_IDandRS_SECRET_KEY. - In the sheet, select an empty cell, for example
Settings!B1. Choose Data > Named ranges and name itRS_REFRESH. - In the script editor, run
installWeeklyRefreshonce. Google asks you to authorize the trigger and spreadsheet access. The custom functions do not need this, but the trigger does. - In the sheet, enter
=IMAGE(SCREENSHOT_URL(A2, "og_card", "", RS_REFRESH)).
// RenderScreenshot for Google Sheets: signed screenshot URLs for IMAGE(). // Script properties (Project Settings > Script properties): // RS_PUBLIC_KEY_ID rs_pub_... // RS_SECRET_KEY rs_secret_... const RS_ENDPOINT = 'https://api.renderscreenshot.com/v1/screenshot'; const WEEK_SECONDS = 7 * 24 * 3600; /** * Returns a signed screenshot URL for a web page. Wrap it in IMAGE(). * * @param {string} url The page to capture, starting with https:// * @param {string} preset Optional preset, for example "og_card" or "link_preview" * @param {string} options Optional extra GET parameters, for example "format=webp&block_cookies=true" * @param {any} refresh Optional cell that a weekly trigger changes, so links are re-signed * @return The signed URL * @customfunction */ function SCREENSHOT_URL(url, preset, options, refresh) { if (!url) return ''; const params = parseOptions_(options); params.url = String(url).trim(); if (preset) params.preset = String(preset); return signedUrl_(params); } /** * Returns a signed URL that renders a short HTML snippet as an image. * * @param {string} html The HTML to render * @param {number} width Viewport width in pixels * @param {number} height Viewport height in pixels * @param {any} refresh Optional cell that a weekly trigger changes * @return The signed URL * @customfunction */ function HTML_IMAGE_URL(html, width, height, refresh) { if (!html) return ''; return signedUrl_({ html: String(html), width: width || 1200, height: height || 630 }); } function signedUrl_(params) { const props = PropertiesService.getScriptProperties(); const keyId = props.getProperty('RS_PUBLIC_KEY_ID'); const secret = props.getProperty('RS_SECRET_KEY'); if (!keyId || !secret) throw new Error('Set RS_PUBLIC_KEY_ID and RS_SECRET_KEY in Script properties'); // Reuse an earlier identical render for 0 credits, for up to 30 days. params.cache_available = 'true'; params.cache_ttl = '2592000'; // Same expiry for a whole week, so the URL (and the image Sheets caches) stays the same. // It lands 21 to 28 days ahead, inside the 30-day maximum. const now = Math.floor(Date.now() / 1000); params.expires = String(Math.floor(now / WEEK_SECONDS) * WEEK_SECONDS + 4 * WEEK_SECONDS); params.key_id = keyId; const query = Object.keys(params) .map(function (k) { return encode_(k) + '=' + encode_(params[k]); }) .sort() .join('&'); const bytes = Utilities.computeHmacSha256Signature(query, secret); const hex = bytes.map(function (b) { return ('0' + (b & 0xff).toString(16)).slice(-2); }).join(''); return RS_ENDPOINT + '?' + query + '&signature=' + hex; } // Encode like the API: space -> "+", only A-Z a-z 0-9 - _ . ~ stay as they are. function encode_(value) { return encodeURIComponent(String(value)) .replace(/[!'()*]/g, function (c) { return '%' + c.charCodeAt(0).toString(16).toUpperCase(); }) .replace(/%20/g, '+'); } function parseOptions_(options) { const out = {}; if (!options) return out; String(options).split('&').forEach(function (pair) { const i = pair.indexOf('='); if (i > 0) out[pair.slice(0, i).trim()] = pair.slice(i + 1).trim(); }); return out; } // Run once from the editor: creates the weekly trigger that re-signs every link. function installWeeklyRefresh() { ScriptApp.newTrigger('bumpRefreshCell').timeBased().everyWeeks(1).onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(3).create(); bumpRefreshCell(); } // Writes the current date into the named range RS_REFRESH. Formulas that pass it as `refresh` recalculate. function bumpRefreshCell() { const range = SpreadsheetApp.getActive().getRangeByName('RS_REFRESH'); if (!range) throw new Error('Create a named range called RS_REFRESH (one cell)'); range.setValue(new Date().toISOString().slice(0, 10)); }
Why the expiry is rounded to the week
Every signed URL needs expires, at most 30 days ahead. If the function used "now + 30 days", each recalculation would make a new URL. Sheets would fetch the image again, and the link would change for no reason. The script instead rounds to the start of the current week and adds four weeks. All cells signed in the same week get the same URL, and each link stays valid for 21 to 28 days.
A custom function runs again only when its arguments change. Google does not let you pass NOW() to one. This is why each formula takes RS_REFRESH as its last argument. The weekly trigger writes the date into that cell, and every formula that refers to it is signed again. If the trigger fails once, the old links still work for three more weeks.
Parameter mapping
| Function argument | API parameter | Example |
|---|---|---|
url |
url |
A2, or "https://example.com" |
preset |
preset |
"og_card", "link_preview", "mobile". See presets. |
options |
any flat GET parameter | "format=webp&block_cookies=true&dark_mode=true" |
refresh |
none | RS_REFRESH, only to start a recalculation |
| (fixed) | cache_available, cache_ttl |
true and 2592000, so repeat fetches cost 0 credits for 30 days |
options takes the names from the GET endpoint reference. Write booleans as true. Options without a GET name, such as wait-for-selector or cookies, cannot be signed into a URL.
For HTML, use =IMAGE(HTML_IMAGE_URL("<h1>"&A2&"</h1>", 1200, 630, RS_REFRESH)). Keep the HTML short: the whole document travels in the URL.
Where the key lives, and who can read it
The secret key is in Script properties, not in a cell. Viewers and commenters cannot open the Apps Script project. Editors can, and they can read the properties. Share edit access only with people you would trust with the signing key. If an editor leaves, revoke the pair in the dashboard; revoking one key of a pair revokes both.
The URLs in the cells are public in a smaller way. Each one renders exactly the page and options it was signed for, until it expires. Nobody can change its parameters without breaking the signature.
Errors and retries
IMAGE() shows an empty cell for any failed image and does not show the reason. To see it, copy the URL with =SCREENSHOT_URL(A2) in a spare cell and open it in a browser tab. The JSON error names the code.
| Code | Cause in Sheets | Fix |
|---|---|---|
| invalid_signature | The URL was edited after signing, or the properties hold the wrong secret | Do not change the returned URL. Check both properties. |
| expired_signature | The weekly trigger stopped and the cells were not signed again | Run bumpRefreshCell by hand, then check Triggers |
| invalid_url | The cell holds example.com without https:// |
Use "https://"&A2 |
| toomanyrequests | The sheet asked for more images in one minute than your plan allows | See limits below |
| timeout | The page loads slowly | Add timeout=45 to options |
Limits and costs
- Requests per minute. Each image that Sheets fetches is one request to the API, counted per account. The free plan allows 10 per minute. A new column of hundreds of images can go past that, and the cells that got
429stay empty until Sheets fetches them again. Add images in blocks, or use a plan with a higher limit. See rate limits. - Credits. The first fetch of each URL and option set renders and uses one credit. Later fetches within 30 days reuse that render for 0 credits, even after the weekly re-sign, because the cache key comes from the rendering options. The free plan gives 150 credits each month. See pricing.
- Freshness. The flip side of the 30-day reuse: an image can be up to 30 days old. Lower
cache_ttlin the script if your pages change often. - Apps Script. A custom function must return within 30 seconds. Signing takes far less, because the function only computes an HMAC and never calls the API.
Troubleshooting
#ERROR!with "Set RSPUBLICKEYID and RSSECRET_KEY". The script properties are missing or misspelled.#NAME?. The script is not saved, or the function name has a typo.- The cell shows
Loading...for a long time. A volatile function such asNOW()was passed as an argument. UseRS_REFRESHinstead. - The image shows a cookie wall. Add
block_cookies=truetooptions.