ScreenshotNeo

BlogHow-to

How to Bulk Screenshot URLs from a Google Sheets Column

Use Apps Script to read URLs from a Google Sheets column, capture them with a screenshot API, and write image links or images back with per-row status.

By the ScreenshotNeo team4 October 20269 min read

Direct answer: Put one URL in each row of a Google Sheets column, then use a bound Google Apps Script to read each URL, send it to a browser-based screenshot API, and write the returned screenshot link or image back to the matching row. Apps Script can make HTTP requests and update the sheet; a screenshot service does the browser rendering. UrlFetchApp.fetch() alone fetches an HTTP response—it does not render a page like a browser or take a screenshot. See Google’s UrlFetchApp documentation.

Choose how screenshots should appear

Decide the output before writing the script. The simplest and most reliable sheet output is usually a link per row. Displaying screenshots inside the grid is convenient for review, but it adds image-size and access considerations.

Output How it works Considerations
Screenshot link in a cell Write the returned image URL into a result column. Confirm the link’s access rules and how long the provider retains it. The source material does not establish universal link durability or retention.
Image in a cell Use a supported image formula with a URL, or insert an image through the Sheets API. URL-based insertion requires a publicly accessible image URL. Google documents blob insertion with a maximum supported size of 2 MB. See Sheet.insertImage and blob insertion.
Image file stored elsewhere Save image bytes to storage you control and write its reference to the sheet. Choose access controls and retention deliberately; configure storage separately from the screenshot request.

Set up the spreadsheet

  1. Put a header in row 1, such as URL. Put one complete website URL in each subsequent row of a single column.
  2. Choose columns for the screenshot link, status, and error message. For example, use A for URLs, B for screenshot links, C for status, and D for error details.
  3. Open Extensions → Apps Script to create a script bound to the spreadsheet.
  4. Choose a screenshot API that renders pages in a browser. Read its current documentation for endpoint, authentication, parameters, output format, response codes, quotas, and whether the response contains image bytes or a link.
  5. Run a small sample first. Review redirects, login pages, bot checks, slow pages, and long pages before processing the rest.

Google lists the https://www.googleapis.com/auth/script.external_request authorization scope for external requests. Apps Script will ask you to authorize the script when it first needs permission.

Runnable Apps Script pattern

The following is a complete row-processing pattern for a screenshot API that returns a publicly accessible image URL as JSON, for example {"image_url":"https://…"}. The endpoint and payload are deliberately supplied as configuration: screenshot vendors use different request and response formats, so replace the marked values with the current details from your provider’s documentation. This sample records an error per row, keeps results aligned, and processes only a modest range per run.

const CONFIG = {
  sheetName: 'Sheet1',
  firstDataRow: 2,
  urlColumn: 1,       // A
  imageUrlColumn: 2,  // B
  statusColumn: 3,    // C
  errorColumn: 4,     // D
  batchSize: 10,
  endpoint: 'REPLACE_WITH_SCREENSHOT_API_ENDPOINT',
  apiKey: 'REPLACE_WITH_API_KEY'
};

function screenshotNextBatch() {
  const sheet = SpreadsheetApp.getActive().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 urls = sheet.getRange(CONFIG.firstDataRow, CONFIG.urlColumn, count, 1)
    .getDisplayValues();
  const statuses = sheet.getRange(CONFIG.firstDataRow, CONFIG.statusColumn, count, 1)
    .getDisplayValues();

  let processed = 0;
  for (let i = 0; i < urls.length && processed < CONFIG.batchSize; i++) {
    const row = CONFIG.firstDataRow + i;
    const url = urls[i][0].trim();
    if (!url || statuses[i][0] === 'DONE') continue;
    processed++;

    if (!/^https?:\/\//i.test(url)) {
      writeResult_(sheet, row, '', 'ERROR', 'Enter a complete http:// or https:// URL.');
      continue;
    }

    try {
      const response = UrlFetchApp.fetch(CONFIG.endpoint, {
        method: 'post',
        contentType: 'application/json',
        headers: { Authorization: 'Bearer ' + CONFIG.apiKey },
        payload: JSON.stringify({ url: url }),
        muteHttpExceptions: true
      });
      const code = response.getResponseCode();
      const body = response.getContentText();
      if (code < 200 || code >= 300) {
        throw new Error('Screenshot API returned HTTP ' + code + ': ' + body.slice(0, 500));
      }

      const result = JSON.parse(body);
      if (!result.image_url) throw new Error('Response did not contain image_url.');
      writeResult_(sheet, row, result.image_url, 'DONE', '');
    } catch (error) {
      writeResult_(sheet, row, '', 'ERROR', String(error.message || error));
    }
  }
}

function writeResult_(sheet, row, imageUrl, status, errorText) {
  sheet.getRange(row, CONFIG.imageUrlColumn).setValue(imageUrl);
  sheet.getRange(row, CONFIG.statusColumn).setValue(status);
  sheet.getRange(row, CONFIG.errorColumn).setValue(errorText);
}

For a real provider, adapt the request method, authentication header or query parameters, payload, and response parsing to its current API instructions. Keep the API key out of sheet cells. For a more maintainable deployment, consult current Apps Script properties and security guidance and store credentials in an appropriate script configuration rather than embedding them in shared spreadsheet data.

Resume and retry

Run screenshotNextBatch repeatedly. It skips rows marked DONE and blank URLs, so rows marked ERROR can be retried after you correct the issue. For large sheets, add a time-driven trigger or a continuation mechanism and stop before the Apps Script execution limit. Check current Apps Script quotas and the screenshot API’s limits; there is no universally safe batch size. Start with a small batchSize, then increase only when the job completes reliably.

Optional: request multiple screenshots with fetchAll

Google’s UrlFetchApp.fetchAll(requests) makes multiple HTTP requests. It can reduce the overhead of issuing requests one at a time, but it does not make a vendor’s browser rendering unlimited or bypass either service’s quotas. For a provider that accepts the same POST request shape as the sample, build requests for a small batch and map responses back to the original row indexes:

function screenshotBatchWithFetchAll_(sheet, rows) {
  const requests = rows.map(item => ({
    url: CONFIG.endpoint,
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + CONFIG.apiKey },
    payload: JSON.stringify({ url: item.url }),
    muteHttpExceptions: true
  }));

  const responses = UrlFetchApp.fetchAll(requests);
  responses.forEach((response, index) => {
    const row = rows[index].row;
    try {
      const code = response.getResponseCode();
      if (code < 200 || code >= 300) {
        throw new Error('HTTP ' + code + ': ' + response.getContentText().slice(0, 500));
      }
      const result = JSON.parse(response.getContentText());
      if (!result.image_url) throw new Error('Response did not contain image_url.');
      writeResult_(sheet, row, result.image_url, 'DONE', '');
    } catch (error) {
      writeResult_(sheet, row, '', 'ERROR', String(error.message || error));
    }
  });
}

Pass rows as objects such as {row: 2, url: 'https://example.com'}. Keep each response associated with its input row; do not append results in completion order or a failed request can shift every later result. Check the provider’s guidance on concurrency and rate limits before using parallel requests.

Insert images into the sheet

If the API returns a public image URL, Google Apps Script documents URL-based image insertion with insertImage(url, column, row). For example, after validating the returned URL:

sheet.insertImage(imageUrl, 5, row); // column E, matching data row

The URL must be publicly accessible to use this insertion method. If your provider returns image bytes instead, a blob can be inserted, subject to Google’s documented 2 MB maximum supported blob size. Large screenshots are usually better stored as links than embedded into the spreadsheet. Avoid downloading an image URL unless you have confirmed its access requirements and the response is actually an image.

Use cURL, Python, or Node.js outside Apps Script

These alternatives can render a URL through ScreenshotNeo and save the returned image directly. They are useful when you want to preprocess a spreadsheet export, run a local job, or move orchestration outside Apps Script. See the ScreenshotNeo API documentation for request options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image_file:
    image_file.write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

These examples capture one URL. To process a spreadsheet export, iterate over its URL rows, save each returned image or link with the original row identifier, and record per-URL errors so the job can resume.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Call it from Apps Script using its documented request format, then write the response to the corresponding row. A one-call capture looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

With ScreenshotNeo, cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. See the API docs for parameters and integration details, then sign up for 1,000 free screenshots a month, with no card.

Troubleshooting

Symptom Likely cause Fix
Invalid URL or request rejected A cell is blank, has surrounding text, or lacks an http:// or https:// scheme. Trim the cell and validate the full URL before sending it. Record invalid rows as errors rather than silently skipping them.
HTTP 401 or 403 from the API Missing or invalid credentials, or a permission or account issue. Check the current provider documentation for the correct authentication format and confirm the credential is active. Keep it out of the sheet.
HTTP 400 or missing image URL The request payload or response parser does not match the provider’s current API. Compare endpoint, parameter names, content type, and response shape with current docs. Do not assume every API returns a hosted URL.
Image URL exists but the sheet cannot display it The URL is not publicly accessible, expired, or not an image resource. Use a public URL for URL-based insertion, or use a supported image blob if appropriate. Check provider access and retention behavior.
Some rows remain blank The script stopped at an execution limit, or it skipped blank and completed rows by design. Use status and error columns, reduce batch size, and rerun. Add a continuation or trigger mechanism for larger jobs.
Wrong screenshot beside a URL Results were appended or processed without preserving the source row association. Write each result to its original row index, including on parallel requests.
Screenshot shows a challenge, login wall, or error page The target site blocks automated access, requires authentication, redirects, or is unavailable. Inspect the target page and the screenshot service’s options and policies. Do not treat an HTTP fetch of page HTML as an equivalent browser capture.
Requests are slow or throttled Pages may render slowly, or the API or Apps Script may limit request volume. Use modest batches, check both services’ current limits, and use a provider-supported wait or async workflow where available.

Performance, reliability, and cost

  • Batch size: No safe universal maximum is established. Start small, measure completion behavior in your own workflow, and check current Apps Script and provider limits.
  • Retries: Retrying transient timeouts can help, but avoid infinite retries. Preserve the row status and last error, and retry only selected failed rows.
  • Output size: Links keep the spreadsheet lighter. Embedded images can increase clutter, and blob insertion has a documented 2 MB supported-size ceiling.
  • Idempotency: Mark completed rows and skip them on later runs. If URLs change, clear or invalidate the prior result so a stale screenshot is not mistaken for a fresh one.
  • Cost: The screenshot provider’s pricing and billing behavior determine the capture cost; Apps Script quotas and provider quotas are separate constraints. Check current terms before running a large list.
  • Privacy: URLs sent to a screenshot service are disclosed to that service for rendering. Review provider data handling and avoid putting secrets or private query parameters in URLs unless the service and workflow are appropriate.

FAQ

Can Google Sheets take website screenshots by itself?

The cited Google references document HTTP requests and sheet image insertion. They do not document a built-in browser-rendering screenshot feature. Use a browser-capable screenshot service or renderer for the capture.

Use links for large or frequently refreshed lists. Embed images when in-grid visual review matters and the image URL or blob meets the applicable access and size requirements.

Can I run the whole column in one execution?

That depends on current Apps Script limits, request duration, list size, and provider quotas. Use resumable batches and confirm current limits rather than assuming a universal maximum.

Does fetchAll capture pages faster?

It makes multiple HTTP requests, but does not guarantee faster browser rendering or remove service limits. Use only within the provider’s concurrency guidance and retain a mapping from each request to its source row.