How to use ApiFlash with Google Sheets to screenshot URLs in bulk
Capture URLs from a Google Sheet with ApiFlash using Apps Script. Insert screenshots or links beside each row, track errors, and resume large batches.
Use a Google Apps Script attached to your spreadsheet to read URLs row by row, call ApiFlash, and place each returned screenshot into the same row—or save a screenshot link instead. The script below processes a limited number of rows per run, records status and errors, and can resume later. This workflow combines documented Google Sheets and ApiFlash capabilities; the research for this guide did not find an official ApiFlash Google Sheets integration or test a complete script.
1. Set up the sheet and ApiFlash key
- Create a sheet with these headers in row 1:
URL,Screenshot,Status,Error. Put complete URLs, includinghttps://orhttp://, in column A. - Obtain an ApiFlash access key from your ApiFlash account. Requests require a valid key.
- Open Extensions → Apps Script in the spreadsheet.
- In Apps Script, open Project Settings, enable showing the script properties if needed, then add a script property named
APIFLASH_ACCESS_KEYwith your key as its value. Do not put the key in a sheet cell shared with readers. - Paste the image or link workflow below into the editor and save it. Run
captureNextBatchonce and authorize the spreadsheet and external request access when prompted.
Apps Script’s UrlFetchApp can issue HTTPS requests and expose response codes, content, and blobs. If you define OAuth scopes explicitly in the manifest, include https://www.googleapis.com/auth/script.external_request. The basic attached-spreadsheet script uses built-in Spreadsheet service methods; it does not require the advanced Sheets service. See Google’s Apps Script quickstart for project setup and authorization context.
2. Runnable Apps Script: put screenshot images in rows
This version makes a screenshot request for each non-empty URL whose status is not DONE. It stores the image in the sheet as an over-grid image anchored in column B on that URL’s row, and records row-level status. Set the batch size to a modest number first; each image adds weight to the spreadsheet.
const CONFIG = {
sheetName: 'Sheet1',
firstDataRow: 2,
batchSize: 10,
viewportWidth: 1280,
viewportHeight: 800,
format: 'png',
fullPage: false,
ttlSeconds: 86400
};
function captureNextBatch() {
const key = PropertiesService.getScriptProperties().getProperty('APIFLASH_ACCESS_KEY');
if (!key) throw new Error('Set the APIFLASH_ACCESS_KEY script property first.');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.sheetName);
if (!sheet) throw new Error('Sheet not found: ' + CONFIG.sheetName);
const lastRow = sheet.getLastRow();
if (lastRow < CONFIG.firstDataRow) return;
const count = lastRow - CONFIG.firstDataRow + 1;
const rows = sheet.getRange(CONFIG.firstDataRow, 1, count, 4).getValues();
const props = PropertiesService.getScriptProperties();
let processed = 0;
for (let i = 0; i < rows.length && processed < CONFIG.batchSize; i++) {
const sheetRow = CONFIG.firstDataRow + i;
const url = String(rows[i][0] || '').trim();
const status = String(rows[i][2] || '').trim();
if (!url || status === 'DONE') continue;
processed++;
sheet.getRange(sheetRow, 3, 1, 2).setValues([['RUNNING', '']]);
try {
const params = {
access_key: key,
url: url,
width: CONFIG.viewportWidth,
height: CONFIG.viewportHeight,
format: CONFIG.format,
full_page: String(CONFIG.fullPage),
ttl: String(CONFIG.ttlSeconds)
};
const query = Object.keys(params).map(function (name) {
return encodeURIComponent(name) + '=' + encodeURIComponent(params[name]);
}).join('&');
const response = UrlFetchApp.fetch(
'https://api.apiflash.com/v1/urltoimage?' + query,
{ muteHttpExceptions: true, followRedirects: true }
);
const code = response.getResponseCode();
if (code < 200 || code >= 300) {
throw new Error('ApiFlash HTTP ' + code + ': ' + response.getContentText().slice(0, 500));
}
const blob = response.getBlob().setName('screenshot-row-' + sheetRow + '.' + CONFIG.format);
sheet.insertImage(blob, 2, sheetRow);
sheet.setRowHeight(sheetRow, 120);
sheet.getRange(sheetRow, 3, 1, 2).setValues([['DONE', '']]);
const headers = response.getAllHeaders();
const remaining = headers['X-Quota-Remaining'] || headers['x-quota-remaining'];
if (remaining !== undefined) props.setProperty('APIFLASH_LAST_QUOTA_REMAINING', String(remaining));
const reset = headers['X-Quota-Reset'] || headers['x-quota-reset'];
if (reset !== undefined) props.setProperty('APIFLASH_LAST_QUOTA_RESET', String(reset));
} catch (err) {
sheet.getRange(sheetRow, 3, 1, 2).setValues([
['ERROR', String(err && err.message ? err.message : err).slice(0, 1000)]
]);
}
}
}
ApiFlash’s default response is image data, which the script obtains with getBlob(). Apps Script documents inserting an image from a blob with the Sheet image insertion method; the image is anchored at the specified column and row. See the Apps Script Sheet reference and URL Fetch Service reference.
To resume, run captureNextBatch again. Rows marked DONE are skipped; rows marked ERROR are retried on the next run. To retry only selected failures, change their status back to blank or ERROR as appropriate. The script processes at most batchSize eligible rows per execution, so repeated runs advance through the sheet without keeping one Apps Script execution open for the entire list.
3. Use screenshot links instead of embedding images
Inline images are convenient for visual review, but can make a large sheet cumbersome. ApiFlash supports response_type=json to return a JSON document with screenshot links. For this mode, replace the image request and success-handling block in the script with the following. It stores the link in column B and keeps the same row-level status and error columns.
const params = {
access_key: key,
url: url,
width: CONFIG.viewportWidth,
height: CONFIG.viewportHeight,
format: CONFIG.format,
full_page: String(CONFIG.fullPage),
ttl: String(CONFIG.ttlSeconds),
response_type: 'json'
};
const query = Object.keys(params).map(function (name) {
return encodeURIComponent(name) + '=' + encodeURIComponent(params[name]);
}).join('&');
const response = UrlFetchApp.fetch(
'https://api.apiflash.com/v1/urltoimage?' + query,
{ muteHttpExceptions: true }
);
const code = response.getResponseCode();
if (code < 200 || code >= 300) {
throw new Error('ApiFlash HTTP ' + code + ': ' + response.getContentText().slice(0, 500));
}
const result = JSON.parse(response.getContentText());
const screenshotUrl = result.url || result.screenshot_url;
if (!screenshotUrl) throw new Error('JSON response did not contain a screenshot link.');
sheet.getRange(sheetRow, 2).setValue(screenshotUrl);
sheet.getRange(sheetRow, 3, 1, 2).setValues([['DONE', '']]);
Use the property name returned by the actual ApiFlash JSON response if it differs from the two common candidates shown above. The documentation describes JSON mode as returning screenshot links; inspect a response once before automating downstream use. This link workflow is intended to avoid storing image blobs in cells, but the link’s availability and cache behavior follow ApiFlash’s response and TTL settings.
4. Configure capture behavior
ApiFlash documents a GET endpoint at https://api.apiflash.com/v1/urltoimage and a matching POST endpoint that accepts form data. GET is straightforward for a small attached script; URL parameters must be encoded correctly, as in the example. For unusually long parameter sets, form POST avoids putting all options in the URL. In both cases provide the key and a complete target URL.
| Need | ApiFlash control | Practical choice |
|---|---|---|
| Image format | format: JPEG by default; PNG and WebP are documented alternatives |
Choose a format supported by the intended viewer. Use a consistent format for a batch. |
| Viewport | width and height; documented default is 1920 × 1080 |
Set both explicitly when comparing pages so captures use the same viewport. |
| Long page | full_page |
Enable for a full-page image; expect larger image data and more sheet weight. |
| Image quality | JPEG/WebP quality option | Use a suitable quality setting when file size matters; quality is not relevant to PNG. |
| Content needs time | Up to 10 seconds of delay; the docs advise considering wait_for or wait_until instead of fixed delay when possible |
Wait on a meaningful load condition when available; use a short delay only for known client-side content lag. |
| Lazy-loaded sections | Scrolling option | Enable scrolling when content appears only after scrolling. Check whether the target site requires interaction or authentication. |
| Freshness and reuse | ttl, from 0 to 2,592,000 seconds; default 86,400 seconds |
Use a low TTL for changing pages and a longer TTL when identical repeated captures can reuse a cached result. |
| Response form | Default image bytes or response_type=json |
Use image bytes for row previews; use JSON links for a lighter sheet. |
ApiFlash documents identical requests as eligible for cached results during the TTL; cached screenshots do not count against the monthly quota. Changing any request parameter can make the request different, so keep options stable when you intend cache reuse. For the complete current parameter names and accepted values, use the ApiFlash API documentation.
5. cURL, Python, and Node.js request examples
These standalone examples demonstrate the same image response used by the image-in-cell workflow. Replace the key and target URL. They save the response body as an image after checking the HTTP status; they do not update a Google Sheet. Use the Apps Script implementation above for the spreadsheet workflow.
cURL
curl --fail-with-body -G 'https://api.apiflash.com/v1/urltoimage' \
--data-urlencode 'access_key=YOUR_APIFLASH_ACCESS_KEY' \
--data-urlencode 'url=https://example.com' \
--data-urlencode 'width=1280' \
--data-urlencode 'height=800' \
--data-urlencode 'format=png' \
--output screenshot.png
Python
import requests
params = {
'access_key': 'YOUR_APIFLASH_ACCESS_KEY',
'url': 'https://example.com',
'width': 1280,
'height': 800,
'format': 'png',
}
response = requests.get('https://api.apiflash.com/v1/urltoimage', params=params, timeout=90)
response.raise_for_status()
with open('screenshot.png', 'wb') as image_file:
image_file.write(response.content)
print('HTTP:', response.status_code)
print('Quota remaining:', response.headers.get('X-Quota-Remaining'))
Node.js
const params = new URLSearchParams({
access_key: 'YOUR_APIFLASH_ACCESS_KEY',
url: 'https://example.com',
width: '1280',
height: '800',
format: 'png'
});
const response = await fetch(`https://api.apiflash.com/v1/urltoimage?${params}`);
if (!response.ok) {
throw new Error(`ApiFlash HTTP ${response.status}: ${await response.text()}`);
}
const image = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('screenshot.png', image));
console.log('Quota remaining:', response.headers.get('X-Quota-Remaining'));
6. Batch size, quotas, and resumability
For bulk jobs, separate the work into small executions and persist a status in every source row. The example does this with batchSize, DONE, RUNNING, and ERROR. If an execution stops unexpectedly, a row left as RUNNING can be reset to blank or ERROR and retried. Keep the key in script properties, and avoid logging full request URLs because a GET query includes the access key.
- ApiFlash documents a leaky-bucket rate of 20 requests per second with a burst size of 400. Requests above the rate are delayed; requests beyond the burst can end with HTTP 429. A sequential Apps Script loop is naturally slower than that ceiling, but execution limits, sheet write time, capture time, and your account quota still matter.
- Successful capture responses can include
X-Quota-Limit,X-Quota-Remaining, andX-Quota-Reset. The image script saves remaining quota and reset values as script properties when present. You can inspect them in Apps Script project settings; for an operational dashboard, write the values to a separate summary area instead. - The docs identify a quota endpoint; consult ApiFlash documentation for its exact request form. Do not assume a fixed monthly capacity from the HTTP rate limit.
- Failed captures with exactly the same parameters are limited to five attempts per hour. Avoid tight retry loops. Record the error, correct invalid inputs, and retry later when appropriate.
- Use stable parameters if you want identical requests to hit cache. ApiFlash documents a default TTL of one day and a maximum of 30 days; set a shorter TTL or zero when you need fresh captures.
For a controlled parallel implementation, Apps Script offers UrlFetchApp.fetchAll, but parallelism increases the chance of bursts, 429 responses, simultaneous writes, and partial completion. The sources establish that fetchAll can issue multiple requests; they do not establish a tested concurrency or retry schedule for this particular workflow. Begin sequentially, watch response codes and quota headers, and only add bounded parallel requests if your account and execution environment support the load.
7. Troubleshooting
| Symptom or HTTP code | Likely cause | What to do |
|---|---|---|
| HTTP 400 | Invalid parameter, malformed URL, or target that cannot be captured | Check that the URL includes https:// or http://; validate option names and values; try the URL directly with the smallest request. |
| HTTP 401 | Invalid or revoked access key | Replace the value of APIFLASH_ACCESS_KEY with a valid dashboard key. Keep it out of shared cells and logs. |
| HTTP 402 | Monthly quota exceeded | Check account usage and quota before resuming; wait for reset or adjust the account plan as appropriate. |
| HTTP 403 | Requested feature is not available on the current plan | Review the plan requirements for that option or remove it from the request. |
| HTTP 429 | Request rate or burst limit exceeded | Reduce batch concurrency, pause, and retry gradually. Do not immediately retry every failed row in a tight loop. |
| HTTP 500 | ApiFlash-side capture failure | Save the row error, retry later, and check whether the target itself is consistently unavailable or unrenderable. |
| Rows remain RUNNING | Execution stopped between status update and completion | Inspect the row, clear or reset its status, and rerun. The workflow’s row status is the resume checkpoint. |
| No visible image | Image is anchored over cells, row height is too small, or the returned content is not an image | Check the response code and format, enlarge the row, and inspect the inserted image’s anchor and size. |
| Only the top of a page appears | Viewport capture was used, or lazy content was not loaded | Enable full-page capture or the documented scroll option and confirm the target exposes the content without additional interaction. |
| Image is stale | Identical request reused a cached capture within its TTL | Lower the TTL or set it to zero when a fresh capture is needed; keep a longer TTL when reuse is desirable. |
| JSON link is missing | Response field shape differs from the assumed property, or response was not JSON | Log the JSON body with the key removed, inspect the documented response schema, and update the property lookup. Do not treat a successful HTTP status alone as proof a link was parsed. |
| Authorization prompt or external request error | Script has not been authorized, or an explicit OAuth scope is missing | Run the function as the spreadsheet owner and grant the requested access. Add the external request scope if scopes are managed explicitly. |
8. Performance, reliability, and cost considerations
- Image size: Full-page screenshots and PNGs can be much larger than viewport captures or compressed formats. A link-based sheet avoids embedding every image in the grid, while images are easier to scan visually.
- Capture time: Dynamic pages may need a wait condition, scrolling, or a short delay. Fixed delays increase run time for every row; prefer the documented readiness controls where suitable.
- Reliability: Treat each row as an independent job. Save status and error text before moving on, and retry only failed rows after reviewing the cause. An HTTP success means the request returned successfully, but you should still verify expected image or JSON content.
- Quota and cache: The API rate limit is not the same as the monthly quota. Successful response headers report quota information, and identical requests may use cache without counting against monthly quota.
- Cost: The dossier does not establish ApiFlash plan prices or a per-capture price, so estimate cost from your current account plan and usage page rather than assuming a rate. Reusing cache, choosing only needed pages, and avoiding unnecessary retries reduce avoidable requests.
- Credential exposure: GET requests place the access key in the query string. Do not expose full URLs in logs, copied error reports, or cells visible to spreadsheet viewers. Script properties reduce accidental cell exposure, but spreadsheet editors who can edit the script may still access project configuration.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. For a screenshot, try:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for the request options. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which outcome occurred. Its MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up free for 1,000 screenshots a month, no card required.
Frequently asked questions
Is there an official ApiFlash add-on for Google Sheets?
The research for this guide found no official integration. The Apps Script workflow calls ApiFlash’s documented endpoint from a spreadsheet script.
Can I capture URLs from a different column?
Yes. Change the source range and the column index used for the URL, then keep the output and status columns aligned with your sheet layout.
Can I use this for pages that require a login?
This example sends a target URL and capture settings; it does not implement a logged-in browser session. Check ApiFlash’s current supported authentication and request options before relying on captures of private pages.
Do I need the advanced Google Sheets service?
No. The attached-spreadsheet example uses Apps Script’s built-in spreadsheet methods and URL Fetch service. The advanced Sheets service is optional for this workflow.
Why are duplicate URLs sometimes not charged again?
Identical requests can return a cached screenshot within the configured TTL, and ApiFlash documents cached screenshots as not counting against monthly quota. Different request parameters or an expired TTL can result in a new capture.


