ScreenshotNeo

BlogHow-to

How to Take Website Screenshots Automatically in Google Sheets

Automate URL screenshots in Google Sheets with Apps Script, triggers, batching, error handling, and a faster ScreenshotNeo option.

By the ScreenshotNeo team29 September 202610 min read

How to Take Website Screenshots Automatically in Google Sheets

Direct answer: Google Sheets does not render arbitrary web pages into screenshots by itself. The reliable workflow is to store one URL per row, use a bound Google Apps Script to call a browser screenshot API with UrlFetchApp, then write a status and either an image URL or an image blob back into the sheet. Use a custom menu or button for on-demand captures, and an installable time-driven trigger for recurring jobs.

This guide builds that workflow from scratch, explains the limits that affect it, and shows how to make it dependable for batches. It also shows an API-based alternative when you need full-page rendering, cookie-banner cleanup, JavaScript controls, PDFs, or higher-volume capture.

1. Set up the spreadsheet

Create a sheet named Captures with this header row:

Column Purpose Example
A: URL One public or authenticated target URL per row https://example.com
B: Status Queued, captured, skipped, or an error message Queued
C: Screenshot Image formula or a note containing the result Image appears here
D: Captured at Timestamp for the last attempt 2026-09-29 12:00:00

Keep the URL column free of blank rows inside the range you plan to process. Validate URLs before sending them: require an http:// or https:// scheme, and decide whether redirects, login pages, or private network addresses are allowed for your use case.

2. Create a bound Apps Script

  1. Open the spreadsheet and choose Extensions → Apps Script.
  2. Replace the starter function with the script below.
  3. Save the project and run onOpen once from the editor so Google can request authorization.
  4. Accept the spreadsheet and external-request permissions. Apps Script’s URL Fetch service is designed to access resources on the web through HTTP or HTTPS requests: Google URL Fetch documentation.

The example assumes your provider returns image bytes. Replace the endpoint, authentication fields, and any provider-specific options with the selected service’s current documentation. The API defines the rendering engine, output format, dimensions, and error schema; those fields are not universal.

Apps Script reads URLs, a screenshot service renders each page, and Sheets stores the resulting image or link.
Apps Script reads URLs, a screenshot service renders each page, and Sheets stores the resulting image or link.
const CONFIG = {
  sheetName: 'Captures',
  urlColumn: 1,
  statusColumn: 2,
  imageColumn: 3,
  capturedAtColumn: 4,
  firstDataRow: 2,
  screenshotEndpoint: 'https://your-provider.example/v1/screenshot',
  apiKey: 'REPLACE_WITH_API_KEY'
};

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Screenshots')
    .addItem('Capture pending rows', 'capturePendingRows')
    .addItem('Create hourly trigger', 'createHourlyTrigger')
    .addToUi();
}

function capturePendingRows() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.sheetName);
  if (!sheet) throw new Error(`Missing sheet: ${CONFIG.sheetName}`);

  const lastRow = sheet.getLastRow();
  if (lastRow < CONFIG.firstDataRow) return;

  const rowCount = lastRow - CONFIG.firstDataRow + 1;
  const values = sheet.getRange(CONFIG.firstDataRow, 1, rowCount, 4).getValues();
  const output = values.map(row => [row[1], row[2], row[3]]);

  values.forEach((row, index) => {
    const url = String(row[CONFIG.urlColumn - 1] || '').trim();
    const status = String(row[CONFIG.statusColumn - 1] || '').trim().toLowerCase();
    const sheetRow = CONFIG.firstDataRow + index;

    if (!url || (status && status !== 'queued' && status !== 'retry')) {
      return;
    }
    if (!/^https?:\/\//i.test(url)) {
      output[index][0] = 'Skipped: URL must start with http:// or https://';
      output[index][2] = new Date();
      return;
    }

    try {
      const response = UrlFetchApp.fetch(CONFIG.screenshotEndpoint, {
        method: 'get',
        muteHttpExceptions: true,
        headers: { Authorization: `Bearer ${CONFIG.apiKey}` },
        payload: { url: url, format: 'png' }
      });
      const code = response.getResponseCode();
      if (code < 200 || code >= 300) {
        throw new Error(`Provider HTTP ${code}: ${response.getContentText().slice(0, 300)}`);
      }

      const blob = response.getBlob().setName(`screenshot-${sheetRow}.png`);
      if (blob.getBytes().length > 2 * 1024 * 1024) {
        throw new Error('Image exceeds the supported 2 MB Blob size for insertion.');
      }

      const image = sheet.insertImage(blob, CONFIG.imageColumn, sheetRow);
      image.setAltTextDescription(`Screenshot of ${url}`);
      output[index][0] = 'Captured';
      output[index][2] = new Date();
    } catch (error) {
      output[index][0] = `Error: ${error.message}`;
      output[index][2] = new Date();
    }
  });

  sheet.getRange(CONFIG.firstDataRow, CONFIG.statusColumn, rowCount, 3).setValues(output);
}

function createHourlyTrigger() {
  ScriptApp.newTrigger('capturePendingRows')
    .timeBased()
    .everyHours(1)
    .create();
}

The script reads the URL range in one operation, processes rows, and writes status values in one batch. It inserts each returned blob as an over-grid image. Google documents the URL and Blob insertion methods in the Sheet class reference; the documented supported maximum for an inserted image Blob is 2 MB.

3. Choose how images are stored

Insert an image Blob

A Blob keeps the image data inside the spreadsheet and avoids depending on a public URL later. It is useful for private results, but every image must fit the documented 2 MB supported Blob size. Large full-page PNGs can exceed that limit. JPEG or WebP output may reduce size if your provider supports those formats and your visual requirements allow them.

Insert an image from a URL

If the provider returns a hosted image URL, you can use a cell formula such as =IMAGE("https://public.example/screenshot.png"), or call sheet.insertImage(url, column, row). URL insertion requires the URL to be publicly accessible. A URL that needs an Authorization header, expires quickly, or is blocked by Sheets will not display reliably. Check the provider’s retention and access rules before choosing this design.

For larger files, download the response blob and save it to a Drive folder, then write the file URL or ID into the sheet. Do not assume that a Drive sharing link is publicly readable: configure access deliberately and verify the behavior for every intended viewer. For sensitive screenshots, keep the file private and share the spreadsheet with the same audience.

4. Start captures with a menu, button, or trigger

Manual menu or button

The custom menu in the example is the safest starting point. A user chooses Screenshots → Capture pending rows, and the function runs with authorized access. You can also insert a drawing or image in Sheets, select its menu, choose Assign script, and enter capturePendingRows.

Time-driven trigger

The createHourlyTrigger function creates an installable clock trigger. Installable triggers run as the account that created them. Google may randomize the exact firing time within the selected period, so use the trigger for periodic freshness rather than an exact-minute SLA. Review and remove duplicates under Triggers in the Apps Script editor. See Google’s installable trigger documentation.

Edit trigger

An installable edit trigger can capture a row after a person enters a URL. A simple trigger cannot use services that require authorization, so use an installable trigger when external requests are involved. Also remember that edits made by another script or API do not themselves activate edit triggers. If your integration appends URLs programmatically, call the capture function directly or schedule a separate trigger. Trigger ownership and visibility matter in shared spreadsheets: the creator’s account owns the authorization and maintenance.

Why a custom formula is usually wrong

A formula such as =SCREENSHOT(A2) looks convenient but is a poor job runner for this task. Custom functions must return a value to their calling cell, cannot edit arbitrary cells, may be unable to call services that require authorization, and must finish within 30 seconds. Google documents that limit in Custom Functions in Google Sheets. Use a menu, button, or installable trigger for network capture and image insertion.

5. Make batches reliable

Apps Script execution time, provider latency, provider rate limits, and account quotas all constrain the number of URLs you can process in one run. Google says quotas vary by account type, are per user, and reset 24 hours after the first request. Consult the current quota table before setting a schedule.

  • Process in chunks: add a maximum row count, such as 20 or 50, per invocation. Leave remaining rows as Queued for the next trigger.
  • Batch spreadsheet operations: read a range once and write statuses once. Google’s best-practices guidance recommends minimizing service calls.
  • Retry selectively: retry transient 429 and 5xx responses with exponential backoff. Do not retry invalid URLs, authentication failures, or permanent provider errors without changing the input.
  • Make retries idempotent: use a status column and a capture timestamp so a rerun does not overwrite successful rows accidentally.
  • Record diagnostics: store the HTTP status and a short provider error message in the status cell or a separate log sheet.
  • Separate scheduling from storage: for very large or high-frequency workloads, consider a dedicated database or queue instead of using the sheet as the job system.
function captureNextChunk() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.sheetName);
  const maxRows = 25;
  const lastRow = sheet.getLastRow();
  const rows = sheet.getRange(CONFIG.firstDataRow, 1,
    Math.min(maxRows, Math.max(0, lastRow - CONFIG.firstDataRow + 1)), 4).getValues();
  // Pass this bounded range to the same validation and capture logic.
  // Leave unprocessed rows as Queued for the next invocation.
}

6. Rendering options you should decide before choosing an API

Requirement Question to answer
Page extent Do you need the visible viewport or the complete document?
JavaScript Must client-rendered charts, menus, or lazy images finish before capture?
Authentication Will pages require cookies, custom headers, a user agent, or an Authorization header?
Output Do you need PNG, JPEG, WebP, or PDF, and what dimensions?
Timing Should the capture wait for a selector, a delay, or network idle?
Access Will Sheets fetch a public URL, or will you insert bytes or store files privately?
Scale What are the provider’s rate limits, batch limits, retention, and price?

Test a few representative pages before automating hundreds of rows: a redirect, a page with lazy images, a page requiring login if applicable, and a page that fails or times out. The screenshot provider’s documentation is the authority for its parameters and response format.

7. Troubleshooting common errors

Symptom Likely cause Fix
Authorization error The script has not been authorized, or the trigger belongs to another account. Run the function manually, approve scopes, and verify trigger ownership.
HTTP 401 or 403 Missing, expired, or incorrectly formatted API credential. Follow the provider’s authentication format and keep keys out of cells shared with readers.
HTTP 429 Provider rate limit or Apps Script quota pressure. Reduce concurrency, add backoff, process smaller chunks, and check current quotas.
Image is blank The URL is private, expired, blocked, or the page had not finished rendering. Use a browser-capable provider, wait for a selector or network idle, and verify access credentials.
Inserted image fails The blob is over 2 MB or the URL is not publicly accessible. Use JPEG/WebP, resize the output, store it in Drive, or use a public hosted URL.
Script times out Too many slow captures in one execution. Lower the chunk size and schedule continuation runs.
Edit automation never runs The edit was made by a script/API, or only a simple trigger was configured. Call capture logic directly after programmatic writes or use an installable time trigger.
Duplicate images appear A trigger was created more than once or successful rows remain eligible. Delete duplicate triggers and skip rows whose status is already Captured.

8. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns a PNG, JPEG, WebP, or PDF. Before capture, it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed.

Consent banners and overlays can change the captured result; cleanup options remove them before capture.
Consent banners and overlays can change the captured result; cleanup options remove them before capture.

Use the documented parameters in the ScreenshotNeo API documentation. This minimal call returns a WebP image that Apps Script can download as a blob:

function captureWithScreenshotNeo(url) {
  const endpoint = 'https://api.screenshotneo.com/v1/shot';
  const query = {
    access_key: 'YOUR_API_KEY',
    url: url,
    format: 'webp'
  };
  const response = UrlFetchApp.fetch(endpoint, {
    method: 'get',
    muteHttpExceptions: true,
    payload: query
  });
  if (response.getResponseCode() < 200 || response.getResponseCode() >= 300) {
    throw new Error(`ScreenshotNeo HTTP ${response.getResponseCode()}`);
  }
  return response.getBlob().setName('screenshot.webp');
}

For direct API use outside Apps Script:

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}`);

ScreenshotNeo includes full-page capture with lazy images loaded, element capture by CSS selector, dark mode, 12 device presets and custom viewports, retina scale, custom CSS and JavaScript, clicks, selector or network-idle waits, request and resource blocking, custom headers and cookies, user-agent and Authorization support, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Plans include 1,000 screenshots per month free with no card, Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to start with 1,000 screenshots a month and no card.

9. Performance, reliability, and cost checklist

  • Capture only rows whose URL or content changed when possible.
  • Use WebP or JPEG when a smaller sheet image is acceptable.
  • Use full-page mode only when the complete document is needed.
  • Wait for a meaningful selector instead of using an unnecessarily long fixed delay.
  • Cache stable pages with a provider TTL or a sheet timestamp.
  • Keep API keys in Apps Script project properties or another secret store rather than visible cells.
  • Monitor provider billing separately from Apps Script quotas.
  • For recurring jobs, record the last successful capture and retry only failed rows.
  • For hundreds or thousands of URLs, use bulk APIs, asynchronous jobs, or a queue instead of one long spreadsheet execution.

FAQ

Can Google Sheets take a screenshot without an API?

Not for arbitrary web pages. Sheets can display an image, while Apps Script can fetch external resources; a browser screenshot service or another rendering system must produce the page image.

Can I capture a page behind a login?

Only if the screenshot provider supports the required cookies, headers, or authorization and your use complies with the site’s access rules. Store credentials outside the sheet and test the authenticated flow separately.

Why does my screenshot miss images loaded while scrolling?

A viewport capture may never trigger lazy loading below the fold. Use a provider’s full-page capture or a documented scroll and wait option.

Will a scheduled trigger run at an exact time?

No. Installable clock triggers run periodically, but Google may randomize the exact firing time within the selected interval.

What should I do when one URL fails?

Write the failure and provider status to that row, leave it eligible for a controlled retry if the error is transient, and continue processing the rest of the batch.