How to Schedule Recurring Website Screenshots with URL2PNG
Schedule URL2PNG captures with cron, sign each request, and decide when to bypass the 30-day cache. Includes runnable Python, Node.js, and cURL workflows.
Direct answer: URL2PNG’s documented API does not include a recurring schedule. Run a small API client on a schedule you control, such as cron on a server. Sign each request with your API key and secret, and change the unique parameter when every run must produce a fresh capture. URL2PNG documents a default 30-day cache; retrieving a cached image does not count as a fresh render against plan usage. Check the URL2PNG Quickstart for the current request format and options.
1. Decide what each scheduled run should capture
Start by listing the pages, cadence, output format, and retention you need. A daily capture of five URLs means up to 150 requests in a 30-day month. That is not necessarily 150 fresh renders: URL2PNG says cached retrievals do not count against the plan. If the archive must show the page as it appeared at each run, make each request unique. If reuse of an earlier capture is acceptable, let the cache serve it.
| Decision | Choose this when |
|---|---|
| Fresh capture each run | You need a dated visual record; include a changing unique value, such as the current Unix timestamp. |
| Cache reuse is acceptable | You want to avoid repeatedly rendering an unchanged target; omit unique and account for the documented default TTL of 2,592,000 seconds (30 days). |
| Where the job runs | Use a machine that stays available at the desired time. A laptop cron job will not run while the laptop is off. |
| Where images go | Choose a persistent archive location and a retention policy; the examples below save files locally, which may be unsuitable for ephemeral job runners. |
The schedule, storage, monitoring, and retention are your workflow; the API documentation describes capture requests rather than a scheduler or archive. Protect the secret key: request signing depends on it, so keep it in the job runner’s environment or secret store and never put it in browser code or public logs.
2. Create URL2PNG credentials
URL2PNG’s Quickstart describes a request containing an API key, the URL-encoded target and options, and a token. The token is the lowercase MD5 hash of the complete query string followed by your secret key. The query string used for signing must be exactly the one sent in the request: parameter order, encoding, and values matter.
Set these environment variables on the machine that runs the job:
export URL2PNG_API_KEY='PXXXXXXXXXXXXXXXX'
export URL2PNG_SECRET='SXXXXXXXXXXXXXXXX'
Use your real credentials in your environment’s secret manager in production. Avoid printing the signed URL: it contains the token. The examples use HTTPS and the documented v6 endpoint.
3. Write a scheduled capture client
Python client
This script signs a canonical, sorted query string, requests a fresh full-page image, checks for an HTTP error, and saves the response. Install the dependency with python -m pip install requests. Save the script as capture.py and set the target URL and output directory as needed.
import hashlib
import os
import time
from pathlib import Path
from urllib.parse import urlencode
import requests
API_KEY = os.environ["URL2PNG_API_KEY"]
SECRET = os.environ["URL2PNG_SECRET"]
TARGET_URL = os.environ.get("TARGET_URL", "https://example.com/")
OUTPUT_DIR = Path(os.environ.get("OUTPUT_DIR", "screenshots"))
# A changing value makes each scheduled run a distinct cache key.
params = {
"fullpage": "true",
"unique": str(int(time.time())),
"url": TARGET_URL,
"viewport": "1280x1024",
}
query_string = urlencode(sorted(params.items()))
token = hashlib.md5((query_string + SECRET).encode("utf-8")).hexdigest()
request_url = f"https://api.url2png.com/v6/{API_KEY}/{token}/png/?{query_string}"
response = requests.get(request_url, timeout=120)
response.raise_for_status()
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
outfile = OUTPUT_DIR / f"capture-{int(time.time())}.png"
outfile.write_bytes(response.content)
print(f"Saved {outfile}")
Run it once manually before scheduling it:
TARGET_URL='https://example.com/' python capture.py
The timestamp is expressed in seconds here, so two runs in the same second would share a value. If your scheduler can trigger overlapping or near-simultaneous runs, use a higher-resolution timestamp or a unique run identifier. Do not use a predictable value as a security measure; unique is for cache behavior, while the token authenticates the request.
Node.js client
This version uses built-in Node modules and global fetch (Node.js 18 or newer). Save as capture.mjs. It writes the returned bytes to a timestamped PNG.
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
const apiKey = process.env.URL2PNG_API_KEY;
const secret = process.env.URL2PNG_SECRET;
if (!apiKey || !secret) throw new Error('Set URL2PNG_API_KEY and URL2PNG_SECRET');
const targetUrl = process.env.TARGET_URL ?? 'https://example.com/';
const params = new URLSearchParams();
for (const [key, value] of Object.entries({
fullpage: 'true',
unique: String(Date.now()),
url: targetUrl,
viewport: '1280x1024',
}).sort(([a], [b]) => a.localeCompare(b))) {
params.append(key, value);
}
const queryString = params.toString();
const token = createHash('md5').update(queryString + secret, 'utf8').digest('hex');
const endpoint = `https://api.url2png.com/v6/${apiKey}/${token}/png/?${queryString}`;
const response = await fetch(endpoint, { signal: AbortSignal.timeout(120_000) });
if (!response.ok) throw new Error(`URL2PNG returned HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
await mkdir('screenshots', { recursive: true });
const filename = `screenshots/capture-${Date.now()}.png`;
await writeFile(filename, bytes);
console.log(`Saved ${filename}`);
cURL from a signed URL
cURL can download a capture, but the token must be computed over the exact query string first. Use the Python client above to construct and sign requests if you do not already have a signing helper. Once you have the endpoint URL, download it like this:
curl --fail --show-error --silent \
'https://api.url2png.com/v6/YOUR_API_KEY/YOUR_TOKEN/png/?fullpage=true&unique=UNIX_TIMESTAMP&url=https%3A%2F%2Fexample.com%2F&viewport=1280x1024' \
--output capture.png
Replace the placeholders and ensure the token was signed over exactly fullpage=true&unique=...&url=...&viewport=..., including that order and URL encoding. Do not copy an example token: it is specific to the query and secret.
4. Schedule the script with cron
On a Unix-like host with cron installed, use an absolute path to the script and interpreter. For example, to capture at 02:15 UTC every day:
15 2 * * * URL2PNG_API_KEY='PXXXX' URL2PNG_SECRET='SXXXX' TARGET_URL='https://example.com/' /usr/bin/python3 /opt/screenshot-job/capture.py >> /var/log/screenshot-job.log 2>&1
Edit the crontab with crontab -e. Cron commonly runs with a limited environment, so specify absolute executable and file paths. The example places credentials in the crontab for clarity; prefer a protected environment file or the hosting platform’s secret manager, and restrict access to logs and configuration.
Common cron schedules:
| Cadence | Expression |
|---|---|
| Every hour | 0 * * * * |
| Every 6 hours | 0 */6 * * * |
| Daily at 02:15 | 15 2 * * * |
| Weekdays at 08:00 | 0 8 * * 1-5 |
| First day of each month at 03:00 | 0 3 1 * * |
Cron uses the machine’s configured timezone, which may differ from UTC. Confirm the host timezone or configure it deliberately, and account for daylight-saving changes if the capture time must follow a local clock. On managed job schedulers, translate the cadence to that platform’s syntax; scheduling systems vary.
5. Configure capture options and freshness
The Quickstart documents these useful query options. Check the live docs for accepted values and defaults before depending on them.
| Option | Use | Scheduling note |
|---|---|---|
url |
URL-encoded target page. | Include the full scheme, such as https://, and encode it through a query builder rather than concatenating raw input. |
fullpage |
Capture the full page height rather than only the viewport. | Long pages take longer and produce larger images. |
viewport |
Set viewport dimensions as widthxheight; the Quickstart example documents a maximum of 5000×5000 and a default of 1280×1024. |
Keep dimensions consistent across runs if you intend to compare images. |
thumbnail_max_width |
Scale the resulting image to a maximum width. | Useful for smaller archives, but scaling changes the output dimensions and detail. |
unique |
Vary the cache key to force a distinct request; the docs suggest a timestamp. | Use a new value on each run that needs a fresh render. A stable value can reuse a cached result. |
delay |
Wait after document readiness and asset loading before capture. | Use a modest delay for pages that finish rendering after load; excess delay adds runtime. |
| Custom CSS | Inject CSS into the captured page. | Can hide dynamic or irrelevant elements; sign the complete resulting query as usual. |
| User agent and accepted language | Control the browser identity or language sent to the site. | Keep values fixed when comparing screenshots, since they can change rendered content. |
The API accepts query parameters, so any option included in the URL must also be included in the string used to compute the token. URL encoding must happen before signing. The example clients sort key-value pairs and encode them consistently; if adapting them, do not independently rebuild the request query in a different order or encoding.
6. Capture multiple URLs and retain an archive
For several pages, call the API once per target URL and save each image under a stable, safe filename plus a timestamp. Keep the target list in a configuration file or database rather than hard-coding credentials into the script. A practical archive path might group by page identifier and date, for example screenshots/home/2026-10-04T021500Z.png.
For a robust recurring job:
- Use a persistent disk or upload files to your chosen object storage after capture. Local files on disposable runners may disappear after the job ends.
- Write to a temporary filename, then rename only after a successful response so an interrupted job does not leave a misleading final file.
- Record the target identifier, scheduled time, request outcome, and output path. Keep secrets and signed URLs out of logs.
- Set a retention policy so a daily archive does not grow without bound.
- Decide how to handle missed or overlapping runs. A lock or scheduler concurrency limit can prevent duplicate jobs from racing over the same destination.
- For visual monitoring, compare only captures with matching viewport, language, user agent, and relevant options; otherwise configuration changes may look like website changes.
7. Estimate cost and runtime
Estimate fresh-render demand as number of URLs × runs per month when each run uses a new unique value. For example, 12 URLs captured every six hours for a 30-day month require 12 × 4 × 30 = 1,440 fresh captures. This is arithmetic planning, not a URL2PNG usage guarantee. If you allow cache reuse, fewer requests may become fresh renders. URL2PNG states cached loads do not count against plan usage and gives a default 30-day TTL.
The plans page currently lists Bootstrapped at $29/month for 5,000 fresh screenshots, Traction at $99/month for 20,000, and Killinit at $199/month for 50,000. It lists additional screenshot rates of $0.006, $0.005, and $0.004 respectively. The page says there is no free account. These prices and limits can change, so confirm the current URL2PNG plans before choosing a tier.
Runtime depends on the target page, rendering and network conditions, capture dimensions, and any delay you request. Full-page images and extra delay can lengthen a job. Set a sensible client timeout, allow enough time in the scheduler, and monitor job failures rather than assuming every scheduled invocation succeeded.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Authentication or invalid token response | The token does not match the complete query string and secret, often because parameter order or URL encoding changed. | Build the query once, sign that exact encoded string, and send the same string. Verify the correct API key and secret are loaded. |
| Image is stale on every run | The request is reusing a cached URL/options combination. | Add a changing unique value to the signed query. Ensure it changes for each run. |
| Scheduled job never runs | Cron syntax, timezone, executable path, permissions, or environment differs from an interactive shell. | Check the crontab and system logs, use absolute paths, make the script readable/executable, and pass required environment values securely. |
| Script runs manually but fails in cron | Cron has a minimal PATH and does not load shell profile files. |
Use full paths for Python/Node and the script; explicitly configure the working directory and environment. |
| Downloaded file is HTML or an error body | The API returned an error response or an intermediary response rather than an image. | Enable HTTP status checking, inspect response status and content type without logging credentials, and correct the request before saving. |
| Token differs despite same apparent parameters | One client encodes spaces, ampersands, slashes, or Unicode differently, or parameters are sorted differently. | Use a standard URL encoder and sign its output verbatim. Avoid manually encoding the target and then encoding it again. |
| Capture misses late-loading content | The page renders content asynchronously after the default readiness point. | Use the documented delay option where appropriate, then test the page manually and keep the delay as short as the target allows. |
| Job overlaps or overwrites output | A previous capture is still running or filenames are not unique. | Set scheduler concurrency controls, use unique timestamped names, and write to a temporary path before finalizing. |
| Unexpected plan usage | Each changing unique value causes a fresh render, or more URLs/runs are scheduled than estimated. |
Count fresh captures per month, inspect the current plan rules, and omit cache-busting only when cached imagery is acceptable. |
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A single request can return an image or PDF, and you can choose a cache TTL. For an automated job, call the endpoint on your schedule and save its response. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does URL2PNG run the recurring schedule for me?
The researched Quickstart documents API requests, not a recurring scheduler. Arrange an external scheduler to invoke your client.
Will a daily request always create a new image?
Only when the request is distinct for caching purposes. Set a changing unique value when freshness matters.
Can I schedule screenshots from my laptop?
Yes, if its scheduler is active and the machine is awake and connected at run time. For dependable unattended captures, use an always-on host or managed scheduler.
Do I need a different API key for each page?
The documented model uses an API key and signed request; the plans FAQ says accounts are not limited to one domain. Keep credentials private and sign each page’s complete query.
What timezone does cron use?
Typically the host’s configured timezone. Check the host and scheduler documentation, especially if the intended cadence follows local time across daylight-saving changes.


