ScreenshotNeo

BlogHow-to

How to Save Scheduled Website Screenshots to Google Drive with Apps Script in India

Schedule website captures in India with Apps Script, send each URL to a browser renderer, and save the returned image to Google Drive.

By the ScreenshotNeo team4 October 202612 min read

Short answer: Apps Script can schedule the job and save image files to Drive, but its UrlFetchApp service makes HTTP requests; it is not documented as a visual browser renderer. For a screenshot of a rendered webpage, call a separate screenshot-rendering service from the scheduled script, check the response, and save its image bytes as a Drive file. Set the trigger timezone to Asia/Kolkata for an India-local schedule, and plan for approximate trigger timing rather than exact cron timing.

This guide shows the setup, a complete Apps Script example using ScreenshotNeo’s API, a provider-neutral integration pattern, failure handling, and the limits to account for. Google’s documentation describes UrlFetchApp as the service for fetching resources and communicating with other hosts, and DriveApp can create a file from a blob.

1. Understand the workflow

  1. An installable time-driven trigger runs an Apps Script function.
  2. The function sends the target URL to a screenshot renderer over HTTPS.
  3. The renderer returns image data. The script checks the HTTP status and response content type.
  4. The script names the image and creates a file in the chosen Google Drive folder.
  5. The script records success or failure without writing API secrets to logs.

The renderer is an external dependency. Before using one, check its current authentication method, rendering and viewport options, output formats, limits, data handling, pricing, and availability for your use case. The Google documentation cited here establishes the Apps Script and Drive portions; it does not endorse a particular renderer.

2. Prepare the Apps Script project and Drive folder

  1. Create a project at Google Apps Script. A standalone project is suitable for a scheduled job.
  2. Create or choose a Drive folder for screenshots. Copy its folder ID from the folder URL; the ID is the part after /folders/.
  3. In the project settings, set the time zone to Asia/Kolkata. India uses one civil time zone, UTC+05:30. If the script reads or writes a spreadsheet, check that spreadsheet’s timezone separately; it can differ from the script timezone.
  4. Store credentials in Script Properties rather than in source code. In Apps Script, open Project Settings, find Script Properties, and add SCREENSHOTNEO_API_KEY and SCREENSHOT_FOLDER_ID. Use your own values.
  5. Set the target site in the code below. Use a complete HTTPS URL, including any path or query string that should be captured.

On its first manual run, Apps Script will request authorization for external requests and Drive access. Review and grant the required scopes for the account that owns the script and trigger. If scopes are explicitly declared in appsscript.json, include https://www.googleapis.com/auth/script.external_request and an appropriate Drive scope, such as https://www.googleapis.com/auth/drive.

3. Add the capture and Drive-save code

This runnable Apps Script example requests a WebP capture from ScreenshotNeo, validates the response before saving, and uses an India-local timestamp in the filename. See the ScreenshotNeo API documentation for the current request options. Replace the example target URL if needed.

const TARGET_URL = 'https://stripe.com';
const TIME_ZONE = 'Asia/Kolkata';

function captureAndSave() {
  const props = PropertiesService.getScriptProperties();
  const apiKey = props.getProperty('SCREENSHOTNEO_API_KEY');
  const folderId = props.getProperty('SCREENSHOT_FOLDER_ID');
  if (!apiKey || !folderId) {
    throw new Error('Set SCREENSHOTNEO_API_KEY and SCREENSHOT_FOLDER_ID in Script Properties.');
  }

  const endpoint = 'https://api.screenshotneo.com/v1/shot';
  const query = [
    'access_key=' + encodeURIComponent(apiKey),
    'url=' + encodeURIComponent(TARGET_URL),
    'format=webp'
  ].join('&');
  const response = UrlFetchApp.fetch(endpoint + '?' + query, {
    method: 'get',
    muteHttpExceptions: true
  });

  const status = response.getResponseCode();
  const headers = response.getAllHeaders();
  const contentType = String(headers['Content-Type'] || headers['content-type'] || '')
    .toLowerCase();
  if (status < 200 || status >= 300) {
    throw new Error('Screenshot request failed with HTTP ' + status + ': ' + response.getContentText().slice(0, 500));
  }
  if (contentType.indexOf('image/') !== 0) {
    throw new Error('Expected an image response, received ' + (contentType || 'unknown content type'));
  }

  const stamp = Utilities.formatDate(new Date(), TIME_ZONE, 'yyyy-MM-dd_HH-mm-ss');
  const blob = response.getBlob().setName('stripe-' + stamp + '.webp');
  const file = DriveApp.getFolderById(folderId).createFile(blob);
  console.log(JSON.stringify({
    result: 'saved',
    fileId: file.getId(),
    capturedAt: stamp,
    target: TARGET_URL
  }));
}

The code checks for a 2xx response and an image content type before creating a Drive file. It includes only a short portion of an unexpected text response in the thrown error, which can help diagnose a provider error without dumping a potentially large response into logs. Do not log the API key or full authenticated request URL.

ScreenshotNeo’s API supports PNG, JPEG, and WebP output. If changing the requested format, change the filename extension to match. The request parameters used by other screenshot APIs also work with ScreenshotNeo, which can make migration easier; check the docs for the exact option names and supported values.

4. Create the India-local schedule

You can add the trigger in the Apps Script editor or create it once in code. Do not run the trigger-creation function repeatedly: each run creates another trigger.

Option A: Create it in the editor

  1. Run captureAndSave manually once and finish authorization.
  2. Open Triggers in the Apps Script editor and select Add Trigger.
  3. Choose captureAndSave, then select Time-driven and the frequency and time window you want.
  4. Save the trigger. Confirm the project timezone is Asia/Kolkata.

Option B: Create a daily trigger in code

function installDailyTrigger() {
  // Avoid accidentally creating duplicate triggers for this function.
  ScriptApp.getProjectTriggers()
    .filter(trigger => trigger.getHandlerFunction() === 'captureAndSave')
    .forEach(trigger => ScriptApp.deleteTrigger(trigger));

  ScriptApp.newTrigger('captureAndSave')
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .inTimezone('Asia/Kolkata')
    .create();
}

Run installDailyTrigger once, manually, and authorize it. This schedules a daily run in the 9 a.m. hour in the specified timezone; it does not promise execution at precisely 9:00. Google says a recurring 9 a.m. trigger may be assigned a time between 9 and 10 a.m. and then retain that timing. The nearMinute() setting offers a plus-or-minus 15-minute window, not minute-level precision. See Google’s installable triggers guide and ClockTriggerBuilder reference.

5. Use a different screenshot renderer

If you already use another provider, keep the trigger and Drive-storage pattern but replace the endpoint, HTTP method, authentication, payload, and response checks with that provider’s documented contract. This provider-neutral outline is not complete until those details are filled in from the renderer’s current documentation:

function captureWithAnotherRenderer() {
  const response = UrlFetchApp.fetch(RENDERER_ENDPOINT, {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + getRendererApiKey() },
    payload: JSON.stringify({ url: TARGET_URL }),
    muteHttpExceptions: true
  });

  const status = response.getResponseCode();
  if (status < 200 || status >= 300) {
    throw new Error('Renderer returned HTTP ' + status);
  }
  const blob = response.getBlob().setName('capture.png');
  DriveApp.getFolderById(DESTINATION_FOLDER_ID).createFile(blob);
}

Adapt the placeholder names and validate the returned MIME type. Some APIs return JSON containing a download URL instead of image bytes; in that case, parse the documented response and fetch the image separately before saving. Do not assume that every successful HTTP status contains an image.

6. ScreenshotNeo request examples

The same capture can be requested outside Apps Script. These examples use the API base and key format documented by ScreenshotNeo. Keep the key private; do not commit it to a repository or expose it in a public webpage.

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:
    image.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: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

For Apps Script, use UrlFetchApp.fetch(), not the browser or Node.js global fetch(). Apps Script’s V8 runtime is not a browser or standard Node.js environment. See Google’s V8 runtime overview.

7. Configure capture behavior for the target site

The scheduled script controls when capture happens and where the returned file goes. Rendering options belong to the screenshot service. For ScreenshotNeo, the available options include:

  • Output and layout: PNG, JPEG, WebP, or PDF; full-page capture with lazy images loaded; element capture by CSS selector; image resizing; transparent background.
  • Viewport and appearance: 12 device presets or a custom viewport, retina scale, and dark mode.
  • Page readiness: wait for a selector, a chosen delay, or network idle; click an element before capture; hide elements with selectors; add custom CSS or JavaScript.
  • Request and browser context: block ads, trackers, requests, or resource types; supply headers, cookies, user agent, authorization, timezone, and geolocation.
  • Other workflows: cache with a chosen TTL, signed links for public image tags, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage API, and OpenAPI spec.

Only add options the capture needs. For example, use a wait-for-selector option when a known element signals that the important content has loaded; use full-page capture when content below the fold matters. Use the ScreenshotNeo docs for exact parameter names and values. If changing the Apps Script request to include options, URL-encode each query value rather than concatenating raw user-provided strings.

8. India timezone, permissions, and unattended operation

  • Timezone: Set Asia/Kolkata explicitly in the project or trigger. A linked spreadsheet has its own timezone setting; make it match if it displays or computes schedule timestamps.
  • Trigger owner: An installable trigger runs as the Google account that created it. That account must remain authorized and have write access to the destination folder.
  • Drive location: DriveApp supports ordinary Drive operations. For shared-drive-specific behavior, Google directs developers to the advanced Drive service.
  • Scopes and admin policy: The external request needs the UrlFetch scope and file creation needs Drive authorization. A Workspace administrator may restrict Drive access for an organization.
  • Observability: Review Apps Script’s execution history after setup and periodically thereafter. For a multi-site job, log the site, capture time, status, and concise error details to a tracking sheet or other controlled log. Never log secrets.

9. Reliability, performance, and cost

Timing and execution quotas

Google’s current quotas page lists a maximum of six minutes per script execution, 20 triggers per user per script, and daily trigger runtime of 90 minutes for consumer accounts or six hours for Workspace accounts. Quotas can change, so check the live quotas table for the account in use before choosing a capture interval or processing many URLs.

Trigger frequency is not a promise of exact wall-clock timing. A daily trigger is a reasonable fit for periodic archives or visual checks where a window is acceptable. If captures must happen at a precise second, Apps Script time-driven triggers are not an exact scheduler.

Multiple URLs and slow pages

One trigger execution has a six-minute ceiling. Sequentially capturing many slow pages can exceed it. Start with a small batch, measure the actual end-to-end time in your own project, and split a large list across multiple executions or use an asynchronous workflow. ScreenshotNeo supports asynchronous jobs with signed webhooks and bulk capture for up to 100 URLs per call; whether to use those options depends on the workflow and the service’s current documentation. Apps Script still needs to respect its own execution and trigger quotas.

Storage and duplicate files

A recurring job creates recurring files. Use a deterministic naming scheme that includes the site and a timestamp, and decide how long to retain captures. If a rerun should replace a prior snapshot instead of creating another file, implement an explicit lookup/update policy; do not assume that createFile() overwrites an existing file. Consider file size and Drive storage limits for high-frequency or full-page captures.

Retries and cost control

Transient network errors and provider failures can happen. Keep errors visible in execution history, and use bounded retries with backoff only for failures that are safe to retry. A retry may create a duplicate if the first request succeeded but the script failed before recording the Drive file; use a stable capture identifier if deduplication matters. External renderer pricing and Apps Script quotas are separate cost and limit considerations; confirm current provider pricing before scheduling high-volume captures.

10. Troubleshooting

Symptom Likely cause Fix
Trigger runs at the wrong local hour Project or trigger timezone is not Asia/Kolkata, or a spreadsheet uses another timezone. Set the timezone explicitly and check project, trigger, and spreadsheet settings. Remember the trigger may run within an approximate time window.
Manual run works, scheduled run fails The trigger owner lacks authorization, Drive access, or continuing access to the project or folder. Review execution history, reauthorize as needed, confirm folder access for the trigger creator, and recreate the trigger under the intended account if ownership is wrong.
HTTP 401 or 403 from renderer Missing, invalid, or incorrectly transmitted API key; account or endpoint permissions may also apply. Check the renderer’s current authentication documentation and Script Properties. Do not paste keys into logs or source shared with others.
HTTP 400 or 422 Malformed URL or unsupported option/value. Use a complete URL, encode query values, and verify each parameter against the renderer’s documentation.
Saved file is JSON or HTML instead of an image The response may be an error body, or the API may return metadata or a download link. Check status and content type before saving. Parse the documented response format and fetch image bytes if the API returns a URL.
Drive permission or folder error Wrong folder ID, missing Drive scope, or no write access for the trigger owner. Verify the folder ID and account permissions, authorize Drive access, and check whether Workspace policy or shared-drive behavior requires the advanced Drive service.
Execution exceeds six minutes Too many URLs, slow rendering, or excessive waits in one execution. Reduce work per run, avoid unnecessary delay settings, and split the workload or use an asynchronous job pattern.
Duplicate captures appear The trigger was installed more than once or a retry repeated a completed capture. Remove duplicate triggers in the Triggers page and use a stable filename or capture identifier with a deduplication policy.
Target page looks incomplete Client-side rendering, lazy loading, authentication, a consent dialog, or delayed content affected the render. Choose a wait condition, supply required cookies or headers when authorized, or test a suitable full-page and viewport setting. Check the renderer’s supported browser options.

11. Or skip the browser setup

Apps Script can schedule and store captures, but a separate renderer still has to load and render the page. ScreenshotNeo provides a one-request screenshot API and an MCP server for AI agents.

const endpoint = 'https://api.screenshotneo.com/v1/shot';
const query = 'access_key=' + encodeURIComponent('YOUR_API_KEY') +
  '&url=' + encodeURIComponent('https://stripe.com');
const response = UrlFetchApp.fetch(endpoint + '?' + query, {
  muteHttpExceptions: true
});
if (response.getResponseCode() < 200 || response.getResponseCode() >= 300) {
  throw new Error('Screenshot request failed: ' + response.getResponseCode());
}
const image = response.getBlob().setName('stripe-' + Date.now() + '.webp');
DriveApp.getFolderById('YOUR_DRIVE_FOLDER_ID').createFile(image);

Cookie and consent banners are accepted as a visitor and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and make 1,000 screenshots a month without a card.

12. FAQ

Can Apps Script screenshot a page without a separate renderer?

UrlFetchApp fetches HTTP response data; Google does not document it as a browser renderer. To capture a rendered webpage, call a rendering service or operate a browser-rendering system separately.

Will a daily trigger run at exactly 9:00 a.m. in India?

No. A clock trigger runs in an approximate time window. Set Asia/Kolkata to use India local time, but do not treat it as an exact cron schedule.

Can I save PDFs instead of images?

Yes, if the renderer returns PDF bytes and the integration verifies the response and uses a suitable filename and MIME type. ScreenshotNeo supports PDF capture; see its documentation for paper size, margins, landscape, and page-range options.

Can one script capture several websites?

Yes. Store a list of URLs and process a bounded number per run. Account for the six-minute execution limit, renderer latency, trigger runtime quotas, and the cost of the chosen rendering service.