How to Bulk Screenshot URLs from Google Sheets Using Apps Script in India
Build a resumable Google Sheets workflow that captures website screenshots with Apps Script, records results by row, and handles failures and quotas.
To bulk screenshot URLs from Google Sheets, use the sheet as a queue, Apps Script to read rows and make HTTP requests, and a screenshot API to render each web page. Apps Script’s UrlFetchApp can call HTTP and HTTPS services; it is not itself a browser renderer. The script below reads URLs in one range, captures them in manageable chunks, saves image files in Google Drive, and writes each file link and status beside its source URL.
This approach works for India-based developers as a general Google Sheets workflow. The research did not establish any provider’s India-specific availability, pricing, data location, payment support, or latency. Check the current service terms and regional support before relying on a provider.
1. Set up the sheet and Apps Script
Create a sheet named Screenshots with these headers in row 1:
URL | Screenshot link | Status | Error
Put one complete https:// URL in column A per row. Open Extensions → Apps Script, paste the code below, replace the API key placeholder, and save. The code uses ScreenshotNeo’s documented single-shot GET call for each URL. It writes output in batches and stores its next row in Script Properties so you can resume after an execution ends.
Before running, ensure the script can access the spreadsheet and Drive. Apps Script normally detects required authorization scopes. If your project sets scopes explicitly, include https://www.googleapis.com/auth/script.external_request for URL Fetch. Google’s Range methods return rectangular arrays, making a single range read and batched write practical for sheet work. Google Range reference · UrlFetchApp reference.
/**
* Sheet layout: A=URL, B=Screenshot link, C=Status, D=Error.
* Run processScreenshotChunk repeatedly to continue through the queue.
*/
const CONFIG = {
sheetName: 'Screenshots',
firstDataRow: 2,
chunkSize: 20,
apiKey: 'YOUR_API_KEY',
apiUrl: 'https://api.screenshotneo.com/v1/shot',
propertyKey: 'SCREENSHOT_NEXT_ROW'
};
function processScreenshotChunk() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.sheetName);
if (!sheet) throw new Error('Sheet not found: ' + CONFIG.sheetName);
const lastRow = sheet.getLastRow();
const props = PropertiesService.getScriptProperties();
let start = Number(props.getProperty(CONFIG.propertyKey) || CONFIG.firstDataRow);
if (start > lastRow) {
props.deleteProperty(CONFIG.propertyKey);
console.log('Queue complete.');
return;
}
const count = Math.min(CONFIG.chunkSize, lastRow - start + 1);
const urls = sheet.getRange(start, 1, count, 1).getDisplayValues();
const results = [];
for (let i = 0; i < urls.length; i++) {
const source = String(urls[i][0] || '').trim();
if (!source) {
results.push(['', 'SKIPPED', 'URL cell is blank']);
continue;
}
let parsed;
try {
parsed = new URL(source);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('Only http and https URLs are supported');
} catch (e) {
results.push(['', 'INVALID_URL', String(e.message || e)]);
continue;
}
try {
const response = UrlFetchApp.fetch(CONFIG.apiUrl, {
method: 'get',
headers: {},
payload: undefined,
muteHttpExceptions: true,
followRedirects: true,
timeoutSeconds: 90,
// GET query parameters are constructed below to encode the URL safely.
});
// The request above is replaced with a correctly encoded GET URL.
// Kept out of the main URL so nested query strings remain intact.
const unused = response;
const query = '?access_key=' + encodeURIComponent(CONFIG.apiKey) + '&url=' + encodeURIComponent(source);
const shot = UrlFetchApp.fetch(CONFIG.apiUrl + query, {
method: 'get', muteHttpExceptions: true, followRedirects: true, timeoutSeconds: 90
});
const code = shot.getResponseCode();
if (code < 200 || code >= 300) {
const body = shot.getContentText().slice(0, 500);
results.push(['', 'HTTP_' + code, body || 'Screenshot request failed']);
continue;
}
const blob = shot.getBlob().setName('screenshot-row-' + (start + i) + '.webp');
const file = DriveApp.createFile(blob);
results.push([file.getUrl(), 'DONE', '']);
} catch (e) {
results.push(['', 'ERROR', String(e.message || e).slice(0, 500)]);
}
}
// One rectangular write for all result columns in this chunk.
sheet.getRange(start, 2, results.length, 3).setValues(results);
const next = start + count;
if (next > lastRow) {
props.deleteProperty(CONFIG.propertyKey);
console.log('Queue complete.');
} else {
props.setProperty(CONFIG.propertyKey, String(next));
console.log('Chunk saved. Run processScreenshotChunk again to continue at row ' + next + '.');
}
}
/** Reset the saved position to the first URL row to run the queue again. */
function resetScreenshotQueue() {
PropertiesService.getScriptProperties().deleteProperty(CONFIG.propertyKey);
}
Important code correction: In the loop, remove the initial UrlFetchApp.fetch(CONFIG.apiUrl, ...) block and the unused line. The actual screenshot request is the encoded CONFIG.apiUrl + query call. (The final code should contain only that one request.)
When pasted into Apps Script, use normal JavaScript operators: the HTML display above escapes && as && only where HTML requires escaping. The runnable script itself uses &&, <, >, and & operators, not the HTML entities shown in this article markup.
2. Use a clean, runnable request loop
For clarity, here is the request loop in its final form; it replaces the try block inside the previous function. It makes one request per valid URL, checks the HTTP response, and saves successful image bytes as a Drive file. Keep the source URL in column A so each saved result stays tied to its original row.
try {
const query = '?access_key=' + encodeURIComponent(CONFIG.apiKey) + '&url=' + encodeURIComponent(source);
const shot = UrlFetchApp.fetch(CONFIG.apiUrl + query, {
method: 'get',
muteHttpExceptions: true,
followRedirects: true,
timeoutSeconds: 90
});
const code = shot.getResponseCode();
if (code < 200 || code >= 300) {
results.push(['', 'HTTP_' + code, shot.getContentText().slice(0, 500) || 'Screenshot request failed']);
continue;
}
const blob = shot.getBlob().setName('screenshot-row-' + (start + i) + '.webp');
const file = DriveApp.createFile(blob);
results.push([file.getUrl(), 'DONE', '']);
} catch (e) {
results.push(['', 'ERROR', String(e.message || e).slice(0, 500)]);
}
To make this fully runnable, use the preceding complete function with this corrected loop in place of its longer loop request block. HTML code examples encode angle brackets and ampersands for display; in Apps Script source, operators must be literal JavaScript characters.
3. Run, resume, and review the queue
- In the Apps Script editor, select
processScreenshotChunkand click Run. Approve the requested Google permissions. - Inspect the
Screenshotssheet. Each processed row receivesDONE,INVALID_URL,SKIPPED, an HTTP status, or an exception message. - Run the function again to process the next chunk. The saved row position advances only after the chunk’s results are written.
- Use
resetScreenshotQueueto restart from the first URL. Existing output cells are overwritten as rows are processed again.
The Drive file URL is a link that the current Google account can access. It is not automatically a public image URL. If you need public embeds or sharing, configure Drive access deliberately or use a screenshot service’s documented public delivery option. Do not assume private API response bytes can be inserted into a cell as a public image.
4. Choose a request pattern for your volume
| Pattern | When it fits | Trade-off |
|---|---|---|
One fetch() per row in a chunk |
Simple queues where row-level error handling and resumability matter. | Sequential requests can make a chunk take longer; keep chunks small enough to finish. |
UrlFetchApp.fetchAll() |
Several independent requests can be issued together. | Handle request and response mapping carefully; concurrency and provider rate limits still matter. |
| Provider batch endpoint | The chosen provider documents multi-URL submission and result retrieval. | Response formats, maximum URLs, job polling, and retention are provider-specific. Verify its documentation; do not infer them from another service. |
Google documents both fetch() and fetchAll(). A separate screenshot API documented in the research dossier offers a batch endpoint that returns a batch ID, but that is one vendor’s contract, not a universal interface. Check the selected provider’s limits and asynchronous result workflow before building around it.
For large queues, store a stable row identifier or source URL alongside a job ID, and only mark a row complete after its image or result reference has been saved. If rows may be sorted or edited while a run is active, a numeric saved row position can point to a different URL on the next invocation; freeze the queue during processing or track a unique ID per record.
5. Options and configuration to decide
- Chunk size: The example uses 20 rows as a conservative starting configuration, not a benchmark or guaranteed safe maximum. Rendering time varies by target site and API behavior. Reduce it if runs time out; increase only after observing your own execution times and provider limits.
- Timeout: The example requests a 90-second maximum wait per call. Apps Script’s URL Fetch request option supports a timeout; the overall script execution limit still applies.
- Request encoding: Encode both the API key and source URL when building a query string. This protects nested query parameters and reserved characters from changing the request.
- Capture controls: Viewport, full-page behavior, image format, waiting conditions, and other rendering settings depend on the chosen API’s documented parameters. Add only parameters supported by that API.
- Image destination: This example stores returned image bytes in Drive and records the file URL. For a public website, use an explicitly public image URL or a documented signed URL feature; a Drive file URL may require account access.
- Retries: Preserve failed rows and retry only transient conditions such as timeouts or temporary server errors, with a delay and a capped number of attempts. Do not repeatedly retry invalid URLs, authorization errors, or provider-side validation errors without changing the input.
- Request origin: URL Fetch requests originate from Google’s IP ranges. A target site or intermediary with IP allowlists may need to permit those ranges.
6. Quotas, performance, reliability, and cost
Apps Script currently documents a six-minute maximum runtime per execution and daily URL Fetch quotas of 20,000 calls for consumer accounts and 100,000 for Workspace accounts. Google says quotas can change and differ by account type; treat these as documentation values, not a guarantee. A chunked, resumable queue avoids depending on one run finishing an arbitrarily large list. Apps Script quotas.
Batch sheet reads and writes to reduce spreadsheet service calls. Network rendering is usually the part whose duration varies most, but no performance benchmark is available here. The practical throughput depends on page load behavior, chosen capture settings, API concurrency and limits, and Apps Script runtime.
If you use the Google Sheets REST API instead of SpreadsheetApp, its documented quotas are 300 read requests and 300 write requests per minute per project, and 60 per minute per user per project for each. Those are Sheets API quotas, separate from Apps Script URL Fetch quotas. Google advises exponential backoff when quota responses occur. Sheets API usage limits.
Cost has two parts: Apps Script and Sheets quota constraints, plus the screenshot service’s own billing model. Confirm whether the provider charges per request, successful capture, or another unit; how retries and cached responses are treated; and what happens to stored screenshots. For India, separately verify currency, payment acceptance, regional availability, and data handling with the provider.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Authorization or scope error | The script has not been authorized, or explicit scopes omit external requests. | Run from the editor and approve permissions. If scopes are explicitly configured, add script.external_request and the required Sheets/Drive scopes. |
| 401 or 403 from screenshot API | Missing, invalid, or unauthorized access key; possibly an account or plan restriction. | Check the key and the provider’s current authentication documentation. Keep keys out of shared sheets and source repositories. |
| 400 response | Malformed or unsupported URL or parameter. | Use a complete HTTP or HTTPS URL, encode it, and verify the provider’s accepted parameter names. |
| 429 response | Provider or Google API rate limit reached. | Slow submissions, use smaller chunks, and retry with capped exponential backoff. For Sheets API quota errors, follow Google’s backoff guidance. |
| 5xx, timeout, or exception | Temporary provider/network failure or a slow target page. | Keep the row marked for retry, retry later with a limit, and inspect the provider’s response details. Reduce chunk size if the execution approaches six minutes. |
| Link exists but image is inaccessible | The saved Drive file is private to the creating account. | Share it with the intended users or use a documented public/signed image URL. Avoid exposing private captures unintentionally. |
| Apps Script cannot reach a restricted destination | The destination filters source IPs. | Check whether its policy can allow Google’s published URL Fetch IP ranges. |
| Results appear beside the wrong URLs | Rows were sorted or edited while the saved numeric resume position was in use. | Pause edits during processing or store a unique row ID and reconcile results by that ID. |
| Empty or CAPTCHA screenshot | The target returned a bot challenge, consent layer, or content that needs a different wait/render configuration. | Check the provider’s page verdict and supported wait/cookie handling controls. Record the outcome instead of treating every HTTP success as a useful screenshot. |
8. Or skip the browser setup
For a one-off capture, ScreenshotNeo accepts a URL and returns a screenshot. Use its documented options and response behavior at ScreenshotNeo API documentation. ScreenshotNeo is a website screenshot API and MCP server. The Apps Script pattern above can call its single-shot endpoint for each URL; its bulk capture feature supports up to 100 URLs per call, so consult the docs for the current request and result contract before switching to that workflow.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; responses report page verdict and billing headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdffor Claude, Cursor, and other MCP clients. - 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Every feature is on every plan.
Start free with 1,000 screenshots a month and no card.
9. FAQ
Can Apps Script create the screenshot without an API?
Not by itself using UrlFetchApp. It makes HTTP requests; a service that renders the page in a browser must perform the capture.
Can I put the screenshot directly inside a cell?
A cell can display an image from a URL accessible to the spreadsheet viewer, but the response bytes from an authenticated API request are not automatically a public URL. Store the file somewhere with suitable access or record a private file link.
Can I run the job automatically?
Yes. An Apps Script time-driven trigger can call the chunk function periodically. Keep each invocation bounded, and ensure only one run processes the queue at a time to avoid duplicate captures.
Does this workflow require an India-specific script?
No India-specific Apps Script change is established by the research. Provider availability, payment, data region, and latency are separate checks for your account and service.


