ScreenshotNeo

BlogHow-to

How to Automate URL2PNG Screenshots from a Spreadsheet of URLs

Read URLs from a spreadsheet, capture each page with URL2PNG, and write image links and status back to the right rows—with runnable code and troubleshooting.

By the ScreenshotNeo team4 October 202613 min read

Direct answer: read each non-empty URL row from your spreadsheet, make one signed URL2PNG v6 request per URL, save the returned image to durable storage, then write the image link, filename, status, or error back to that same row. URL2PNG documents an individual signed screenshot request; the spreadsheet read/write and storage steps are parts of your integration. For large batches where a ZIP archive is more convenient, url2image documents a separate batch workflow. It is a different service, not a URL2PNG batch endpoint.

This guide uses Python and Google Sheets as a concrete example. The same flow works with another spreadsheet by replacing the sheet read/write calls. Keep the URL2PNG secret outside the sheet and outside source control.

1. Choose the workflow and prepare the sheet

A spreadsheet automation has four jobs: read input rows, capture each URL, persist the image somewhere durable, and record the outcome. URL2PNG returns an image from its screenshot request; your script needs to decide where that image will live. The example below writes image bytes to a local directory and records the local path. For a shared or scheduled workflow, upload those bytes to storage your team already uses, then write its durable image URL to the sheet.

Column Purpose
A: URL Input page address.
B: Image location Path or durable storage URL for the capture.
C: Status done, skipped, or error.
D: Error Short failure detail, if any.

Use a header row in row 1, and put source URLs in column A beginning at row 2. Enable the Google Sheets API and configure application credentials with access to the target spreadsheet. Set credentials using the method supported by your Google client library or runtime; do not put a service account key, URL2PNG secret, or API key in spreadsheet cells. The Google Sheets API supports range reads and writes, including batch operations: Google Sheets API reference.

2. Get URL2PNG credentials and understand signing

URL2PNG v6 uses an API key, a token, and URL-encoded query parameters. Its quickstart describes the token as the MD5 hash of the entire query string concatenated with the secret key. The exact query string used for signing must match the one sent in the request, so parameter ordering and URL encoding matter. Follow the canonicalization and example for your account in the URL2PNG Quickstart Guide; do not assemble one string for signing and let a separate encoder silently change it for transmission.

The URL2PNG documentation includes language examples, including Python, Node.js, and cURL. Since signature construction is sensitive to exact encoding, the runnable Python example below delegates request construction and signing to a small adapter that must match the official quickstart’s canonical query-string procedure. This keeps the spreadsheet and storage logic clear without guessing URL2PNG’s parameter canonicalization. Copy the signing/request construction from the current official quickstart into build_url2png_request and verify it against that guide before running.

3. Run a row-by-row Python capture job

Install the dependencies:

python -m pip install requests google-api-python-client google-auth

Set environment variables for credentials and configuration in your deployment environment. The Google credentials below assume Application Default Credentials are configured and authorized for the target sheet. Implement build_url2png_request using URL2PNG’s current documented example so it returns the fully signed request URL. The function is intentionally isolated because changing query serialization after signing can invalidate every request.

import hashlib
import os
import re
import time
from pathlib import Path
from urllib.parse import urlencode

import requests
from google.auth import default
from googleapiclient.discovery import build

SPREADSHEET_ID = os.environ["SPREADSHEET_ID"]
SHEET_RANGE = os.environ.get("SHEET_RANGE", "Sheet1!A2:D")
URL2PNG_KEY = os.environ["URL2PNG_KEY"]
URL2PNG_SECRET = os.environ["URL2PNG_SECRET"]
OUTPUT_DIR = Path(os.environ.get("OUTPUT_DIR", "captures"))
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)


def build_url2png_request(page_url: str) -> str:
    """Build the signed v6 request exactly as URL2PNG's current quickstart specifies.

    Keep one canonical encoded query string for both token calculation and sending.
    Confirm the required endpoint, parameter names, ordering, and token format in
    https://www.url2png.com/docs before deployment.
    """
    # Example structure only: use the official quickstart's exact v6 signing recipe.
    params = [("url", page_url)]
    canonical_query = urlencode(params)
    token = hashlib.md5((canonical_query + URL2PNG_SECRET).encode("utf-8")).hexdigest()
    # Add the account key and token according to the official quickstart's format.
    # Do not deploy this placeholder URL until it matches the documentation.
    raise NotImplementedError("Insert the exact endpoint and signed parameter format from URL2PNG's quickstart")


def safe_filename(row_number: int, page_url: str) -> str:
    slug = re.sub(r"[^A-Za-z0-9._-]+", "_", page_url)[:80].strip("._") or "page"
    return f"row-{row_number}-{slug}.png"


def main() -> None:
    credentials, _ = default(scopes=["https://www.googleapis.com/auth/spreadsheets"])
    sheets = build("sheets", "v4", credentials=credentials, cache_discovery=False)
    values = sheets.spreadsheets().values().get(
        spreadsheetId=SPREADSHEET_ID,
        range=SHEET_RANGE,
    ).execute().get("values", [])

    updates = []
    session = requests.Session()
    for offset, row in enumerate(values, start=2):
        page_url = row[0].strip() if row else ""
        if not page_url:
            updates.append({"range": f"Sheet1!B{offset}:D{offset}", "values": [["", "skipped", "Empty URL"]]})
            continue
        if not page_url.startswith(("https://", "http://")):
            updates.append({"range": f"Sheet1!B{offset}:D{offset}", "values": [["", "error", "URL must start with http:// or https://"]]})
            continue
        try:
            request_url = build_url2png_request(page_url)
            response = session.get(request_url, timeout=(10, 120))
            response.raise_for_status()
            # Avoid saving an HTML error page as if it were an image.
            content_type = response.headers.get("Content-Type", "").lower()
            if not content_type.startswith("image/"):
                raise ValueError(f"Expected image response, received {content_type or 'unknown content type'}")
            filename = safe_filename(offset, page_url)
            output_path = OUTPUT_DIR / filename
            output_path.write_bytes(response.content)
            updates.append({"range": f"Sheet1!B{offset}:D{offset}", "values": [[str(output_path), "done", ""]]})
        except requests.RequestException as exc:
            updates.append({"range": f"Sheet1!B{offset}:D{offset}", "values": [["", "error", str(exc)[:500]]]})
        except Exception as exc:
            updates.append({"range": f"Sheet1!B{offset}:D{offset}", "values": [["", "error", str(exc)[:500]]]})
        # Tune this delay to the provider's current rate limits and your workload.
        time.sleep(0.1)

    if updates:
        sheets.spreadsheets().values().batchUpdate(
            spreadsheetId=SPREADSHEET_ID,
            body={"valueInputOption": "RAW", "data": updates},
        ).execute()


if __name__ == "__main__":
    main()

Important: this listing is not a fully runnable URL2PNG client until the marked adapter is filled using the official signing example. The dossier does not establish a verified complete Python request recipe, so inventing an endpoint or query format would be unsafe. For a no-adapter implementation, use one of URL2PNG’s current official quickstart examples and transplant the exact signing code into the function.

The script reads one range, validates rows, processes each URL independently, saves successful image bytes, and batch-writes row outcomes. For large sheets, process bounded chunks rather than holding the entire job in memory. Consider writing a status such as processing before capture so an interrupted run can resume by selecting rows without a completed image location.

4. Configure capture behavior

URL2PNG’s documented controls include the following. Parameter names and accepted values should be taken from the current URL2PNG documentation and included in the exact signed query string.

Need Control Practical choice
Capture only the visible viewport fullpage Defaults to false; enable for long-page reviews when a full document image is needed.
Set browser viewport Width and height The docs’ examples list a maximum of 5000×5000. Use the smallest dimensions that answer the task.
Smaller output Thumbnail width Use for contact sheets or review grids; check the documentation for its exact parameter format.
Wait for rendering Delay or “say cheese” element wait Prefer waiting for a known page element when available; use delay only for pages with predictable timing.
Control locale or browser identity Accept-Language and user-agent headers Set only when reproducing a target audience or a specific browser context.
Change page styling Custom CSS Use to hide a specific element or adjust capture presentation; validate selectors against the live page.
Force a fresh render unique Set a unique value only when an updated page must bypass the cached image.

The documented default TTL is 2,592,000 seconds, or 30 days. URL2PNG’s plans page says cached loads inside that cache period do not count as fresh renders. Avoid setting unique on every run unless you need every page re-rendered; doing so defeats the cache’s value and can consume fresh-render allowance.

5. Handle retries, duplicates, and interrupted jobs

  • Make runs resumable: skip rows already marked done unless the URL or capture settings changed.
  • Retry selectively: retry timeouts, rate limits, and transient server errors with bounded exponential backoff and jitter. Do not repeatedly retry malformed URLs, authentication errors, or unsupported parameters.
  • Keep row identity: use a stable row number or a dedicated ID column. If users sort the sheet during a run, row numbers can point to different records; lock editing during the job or write updates by stable ID.
  • Avoid duplicate renders: derive a job key from URL plus capture settings, and record it in the sheet or job database. Use URL2PNG’s cache by default; use unique only for deliberate refreshes.
  • Bound concurrency: start sequentially, then raise parallelism only within the provider’s documented limits. The research dossier does not state a URL2PNG concurrency limit.
  • Bound work per run: use a row range or chunk size, persist each result, and resume on the next scheduled invocation.

For very large workbooks or long-running jobs, put rows into a durable queue and have workers process them. Keep image storage and the status write separate enough that a failed sheet update does not erase the saved image; on retry, detect the existing object and finish the row update.

6. Store images and write useful results

Local files are suitable for a one-person script, but other spreadsheet users cannot open a local path on your machine. For team use, upload each image to durable object storage or an internal file service and write the resulting stable link. Apply your organization’s access controls: page screenshots can contain private account data or personal information. Avoid public links for captures that should remain restricted, and set storage retention to match your need.

Record at least the image link, completion status, and concise error. For auditability, also record capture time and the settings profile used. Avoid putting signed screenshot URLs or credentials into broadly shared columns if those links grant access.

7. cURL and Node.js request patterns

URL2PNG’s official quickstart provides examples for cURL and Node.js. Use those examples for the signed request construction; do not substitute an unsigned URL or assume that parameter ordering is irrelevant. These are integration shapes, with signing deliberately represented as a required step:

# cURL: URL2PNG v6 signed request
# Build the canonical query, token, endpoint, key and token exactly as in the official guide.
curl --fail --location "SIGNED_URL2PNG_REQUEST_FROM_OFFICIAL_QUICKSTART" -o screenshot.png
// Node.js: use the exact token and query-string construction from URL2PNG's official guide.
import fs from "node:fs/promises";

const signedUrl = buildSignedUrlExactlyAsDocumented(pageUrl);
const response = await fetch(signedUrl, { signal: AbortSignal.timeout(120_000) });
if (!response.ok) throw new Error(`Screenshot request failed: ${response.status}`);
const type = response.headers.get("content-type") ?? "";
if (!type.startsWith("image/")) throw new Error(`Expected image, received ${type}`);
await fs.writeFile("screenshot.png", Buffer.from(await response.arrayBuffer()));

The placeholder function in the Node example has the same requirement as the Python adapter: fill it with the exact current official signing procedure before use. URL2PNG’s docs are the authoritative source for the token and request format.

8. Large batches: when a batch interface fits better

If you want to submit hundreds of URLs and download one archive, url2image documents CSV/text uploads and JSON API input, job polling, and ZIP download. Its docs list a maximum of 500 URLs per batch, a 2 MB upload-list limit, and 14-day retention for results and images. They also document refusal of private, loopback, and link-local destinations. This may fit a one-off spreadsheet export and archive workflow; it does not write results back into your Google Sheet automatically, and its endpoint is not part of URL2PNG.

Keep the distinction clear: URL2PNG is documented here as one signed screenshot request per URL, while url2image documents a separate batch-job-and-ZIP interface. The right choice depends on whether you need per-row durable links and URL2PNG capture controls or a submitted batch with an archive. Pricing and quotas change, so verify each provider’s current plan page before budgeting.

9. Performance, reliability, and cost

  • Capture size: full-page and large viewport screenshots produce more image data and can take longer to transfer and store. Select viewport and thumbnail size based on the downstream use.
  • Concurrency: sequential requests are easy to reason about. Parallel workers can reduce wall-clock time, but increase pressure on the provider, spreadsheet API, and storage service. No URL2PNG concurrency or latency benchmark is established by the sources here.
  • Retries: use bounded retries for transient failures; persist each successful image before updating the sheet. This makes recovery less likely to create duplicate work.
  • Cache: the documented 30-day default TTL can avoid fresh renders for unchanged requests. A unique value forces a fresh screenshot, so reserve it for intentional refreshes.
  • Cost: URL2PNG’s plans page listed, when reviewed on 2026-10-03, Bootstrapped at $29/month for 5,000 fresh screenshots, Traction at $99/month for 20,000, and Killinit at $199/month for 50,000. Listed additional-render prices were $0.006, $0.005, and $0.004 respectively. The page said there is no free account and that plans can be canceled month to month. Treat these as dated listings and check the current URL2PNG plans page before purchase.
  • Batch alternative costs: url2image’s reviewed page listed 10 free screenshots per month and prepaid credit packs. Verify current terms on its site before comparing totals.

Estimate fresh captures separately from cache hits. A useful monthly estimate is the number of distinct URLs multiplied by the number of deliberate refresh cycles, adjusted for cache reuse. Also budget for image storage and egress if captures are kept in your own storage; provider screenshot pricing does not cover those costs.

10. Troubleshooting

Symptom Likely cause Fix
Authentication or token error Secret/key mismatch, wrong token input, or query changed after signing. Rebuild the canonical query once, calculate the token from that exact string per the current quickstart, and send the same encoded parameters.
Bad request Unsupported parameter, invalid dimension, or malformed URL. Start with the smallest documented request; add capture options one at a time and validate URL scheme and dimensions.
Image response is actually HTML Provider error page or intermediary response was saved without checking headers. Check HTTP status and content type before writing bytes; retain a short error message for investigation.
Capture shows a loading state Page content was not ready when capture began. Use the documented element-wait option when a stable selector exists; otherwise use a measured delay and keep it as short as practical.
Capture is cut off fullpage is false or viewport dimensions do not match the intended output. Enable full-page capture or set an appropriate viewport; check the documented maximum dimensions.
Same old image appears Cached response within the 30-day TTL. Use the documented unique parameter for a deliberate refresh; omit it for normal repeat runs.
Rows receive the wrong result Rows were sorted or edited while a job was running. Pause edits or update by a stable row ID rather than a row number.
Run stops partway through Process timeout, transient network failure, or a large unbounded batch. Process chunks, persist each successful file, record statuses, and resume only unfinished rows.
Local image paths do not open for colleagues Paths point to the machine running the script. Upload to team-accessible durable storage and write a controlled link to the sheet.

11. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF, and its parameter names also work with those used by other screenshot APIs to ease migration. The call below uses the API base and code pattern from the ScreenshotNeo documentation; replace the example target URL as needed.

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 banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Response headers say which page verdict applied and whether the request was billed.
  • An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots each month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for ScreenshotNeo and try the spreadsheet workflow with 1,000 screenshots a month and no card.

12. FAQ

Does URL2PNG provide a spreadsheet batch endpoint?

The reviewed URL2PNG material documents signed individual requests. The batch-and-ZIP interface described here belongs to url2image.

Can the sheet contain the URL2PNG secret?

No. Store the secret in a protected runtime secret store or environment variable, and restrict access to the automation identity.

Should every scheduled run force a fresh screenshot?

No. Leave uniqueness off when cache reuse is acceptable; force uniqueness only when the page must be recaptured.

Can I write an image into a spreadsheet cell?

The workflow is simpler to maintain when the sheet stores a link or file location and the image bytes live in file or object storage.

What should be rechecked before deployment?

Confirm URL2PNG’s current signing recipe, parameter names, plan limits, and prices in its official docs and plans page; these details can change.