How to Generate Website Thumbnails for a Directory Using the ScreenshotOne API
Build directory previews with ScreenshotOne’s bulk API: size thumbnails, process results safely, and plan for lazy execution, rate limits, storage, and retries.
Use ScreenshotOne’s POST /bulk endpoint to create previews for many directory URLs. Put shared capture settings in options, list each site in requests, and set image_width and image_height to the maximum size your card design needs. The API preserves aspect ratio and keeps the resulting image within those bounds. By default, bulk requests are lazy: the screenshot is rendered when you download its returned URL. Set execute: true when you want captures run before the bulk response comes back. See ScreenshotOne’s bulk API documentation and its screenshot options reference.
Keep your access key on your server and send API requests over HTTPS. The examples below use an illustrative set of directory entries. Treat them as implementation examples based on the documented API behavior, not as a tested application.
1. Choose thumbnail dimensions and capture behavior
First decide what “thumbnail” means in your directory. A card might show a browser-width crop, a compact preview of a full page, or a consistent device viewport. These are different rendering choices:
- Viewport:
viewport_widthandviewport_heightcontrol the browser viewport used to render the page. Choose values that resemble the page presentation you want users to recognize. - Output bounds:
image_widthandimage_heightconstrain the returned image dimensions. The image is resized proportionally; it is not cropped to fill both values. If you provide only one bound, the other is computed to preserve aspect ratio. - Full page: Set
full_page: trueif the preview should include the entire page. Full-page shots can take longer and may produce tall images; use output bounds to keep the card asset manageable. - Format and quality: You can use the regular screenshot options for each entry. For web card previews, choose a format supported by your image pipeline and browser targets, then tune image quality if supported by that format.
- Execution: With
execute: falseor the default lazy behavior, the bulk response gives URLs and the screenshots are captured when those URLs are fetched. Withexecute: true, ScreenshotOne executes the entries before responding. Allow time for the whole batch.
Bulk options are defaults shared across requests. A setting on an individual request overrides the shared value. This lets you keep the viewport and image bounds consistent while making targeted exceptions.
2. Send a bulk request with cURL
Use JSON in a POST body so the list of URLs and shared settings are easy to manage. This example asks the API to execute the screenshots before returning and includes a different viewport override for one entry.
curl --fail-with-body --silent --show-error \
--request POST "https://api.screenshotone.com/bulk" \
--header "Content-Type: application/json" \
--data '{
"access_key": "YOUR_SCREENSHOTONE_ACCESS_KEY",
"execute": true,
"options": {
"viewport_width": 1280,
"viewport_height": 800,
"image_width": 640,
"image_height": 400,
"format": "webp"
},
"requests": [
{"url": "https://example.com"},
{"url": "https://www.python.org", "viewport_width": 1440},
{"url": "https://www.rust-lang.org"}
]
}'
Bulk accepts the access key in the JSON body, as shown, or through the documented X-Access-Key header or query parameter. Avoid putting secrets in URLs that may be logged. Inspect the JSON response and associate each returned result with the directory record at the same input position. Do not assume every target site will render successfully.
3. Run a Python batch and download the images
This script submits a lazy bulk request, then downloads each returned screenshot URL. Install the dependency with python -m pip install requests. Keep the key in an environment variable, for example SCREENSHOTONE_ACCESS_KEY. The script writes files in request order so they can be mapped back to the directory entries.
import os
from pathlib import Path
from urllib.parse import urlparse
import requests
API_URL = "https://api.screenshotone.com/bulk"
ACCESS_KEY = os.environ["SCREENSHOTONE_ACCESS_KEY"]
OUTPUT_DIR = Path("thumbnails")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sites = [
{"id": "example", "url": "https://example.com"},
{"id": "python", "url": "https://www.python.org"},
{"id": "rust", "url": "https://www.rust-lang.org"},
]
payload = {
"access_key": ACCESS_KEY,
"execute": False,
"options": {
"viewport_width": 1280,
"viewport_height": 800,
"image_width": 640,
"image_height": 400,
"format": "webp",
},
"requests": [{"url": site["url"]} for site in sites],
}
with requests.Session() as session:
response = session.post(API_URL, json=payload, timeout=(10, 90))
response.raise_for_status()
result = response.json()
# The bulk response contains a responses array with screenshot URLs.
screenshot_results = result.get("responses", [])
if len(screenshot_results) != len(sites):
raise RuntimeError(
f"Expected {len(sites)} bulk results, got {len(screenshot_results)}"
)
for site, item in zip(sites, screenshot_results):
screenshot_url = item.get("url")
if not screenshot_url:
print(f"No screenshot URL for {site['id']}: {item}")
continue
# Returned screenshot links should be HTTPS. Do not fetch arbitrary
# URLs if this response is being handled in a different trust boundary.
if urlparse(screenshot_url).scheme != "https":
print(f"Refusing non-HTTPS screenshot URL for {site['id']}")
continue
image_response = session.get(screenshot_url, timeout=(10, 90))
if not image_response.ok:
print(f"Download failed for {site['id']}: HTTP {image_response.status_code}")
continue
output_path = OUTPUT_DIR / f"{site['id']}.webp"
output_path.write_bytes(image_response.content)
print(f"Saved {output_path}")
For an eager workflow, change "execute": False to True. The API documents that execution results include a status summary for each request. Check those per-entry outcomes before downloading; a successful bulk HTTP response alone does not mean every target page succeeded.
4. Node.js version
The following example uses the built-in fetch API available in current Node.js releases. It saves the returned image bytes and reports individual download errors.
import { mkdir, writeFile } from "node:fs/promises";
const accessKey = process.env.SCREENSHOTONE_ACCESS_KEY;
if (!accessKey) throw new Error("Set SCREENSHOTONE_ACCESS_KEY first");
const sites = [
{ id: "example", url: "https://example.com" },
{ id: "python", url: "https://www.python.org" },
{ id: "rust", url: "https://www.rust-lang.org" },
];
const payload = {
access_key: accessKey,
execute: false,
options: {
viewport_width: 1280,
viewport_height: 800,
image_width: 640,
image_height: 400,
format: "webp",
},
requests: sites.map(({ url }) => ({ url })),
};
const bulkResponse = await fetch("https://api.screenshotone.com/bulk", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(100_000),
});
if (!bulkResponse.ok) {
throw new Error(`Bulk request failed: HTTP ${bulkResponse.status} ${await bulkResponse.text()}`);
}
const result = await bulkResponse.json();
const entries = result.responses ?? [];
if (entries.length !== sites.length) {
throw new Error(`Expected ${sites.length} results, got ${entries.length}`);
}
await mkdir("thumbnails", { recursive: true });
for (let i = 0; i < sites.length; i += 1) {
const screenshotUrl = entries[i].url;
if (!screenshotUrl || new URL(screenshotUrl).protocol !== "https:") {
console.error(`Missing or unsafe screenshot URL for ${sites[i].id}`);
continue;
}
const imageResponse = await fetch(screenshotUrl, {
signal: AbortSignal.timeout(100_000),
});
if (!imageResponse.ok) {
console.error(`${sites[i].id}: download failed with HTTP ${imageResponse.status}`);
continue;
}
await writeFile(`thumbnails/${sites[i].id}.webp`, Buffer.from(await imageResponse.arrayBuffer()));
console.log(`Saved thumbnails/${sites[i].id}.webp`);
}
5. Connect screenshots to directory records
A production directory should treat thumbnail generation as a background job. A request handler can validate the submitted directory entry and enqueue work; a worker can call the screenshot API and save the result. This avoids keeping a user-facing request open while many pages render.
- Validate and normalize inputs. Require an explicit HTTP or HTTPS URL, reject malformed values, and apply your product’s policy for private or internal hosts. If users can submit arbitrary URLs, defend the capture worker against server-side request forgery and avoid fetching returned URLs from untrusted sources.
- Store a stable record mapping. Persist the directory record ID beside each requested URL and bulk result index. Do not use the URL alone as the identity; a listing can change its target over time.
- Process failures independently. Mark each entry as pending, ready, or failed. A blocked or unavailable site should not prevent other directory cards from receiving thumbnails. Display a neutral fallback image for failed captures.
- Save durable assets yourself. If thumbnails must remain available for directory pages, download successful image bytes and write them to storage you control. The screenshot links returned by ScreenshotOne are not a permanent archive.
- Refresh intentionally. Regenerate when a listing URL changes or according to a refresh schedule that matches how often directory previews need to change.
6. Pick bulk, queue, cache, and storage behavior
| Choice | Use it when | Trade-off |
|---|---|---|
| One bulk request | You have a manageable list and want the API to package shared settings and URL entries. | It is a wrapper around regular screenshot requests; the requests still use the same one-minute request bucket. |
| Controlled queue | You need durable retries, per-site status, or paced throughput for a large directory. | You own queue state and retry policy, but can handle failures and restarts at the URL level. |
| Lazy bulk response | You want returned screenshot URLs and will fetch them as a later step. | Rendering happens when the screenshot URLs are downloaded, so the initial response does not mean images are ready. |
execute: true |
You need execution before the bulk API returns its result. | Wait for the whole batch and inspect each execution status; this can make the request take longer. |
| ScreenshotOne cache | You repeatedly request the same rendering and want cache behavior to reduce repeated rendering. | Caching is best-effort and has a TTL; it is not durable storage for your directory. |
| Your own storage | You need stable asset URLs and control over retention and delivery. | You need to download, store, and refresh files yourself. |
ScreenshotOne documents that screenshots are rendered on demand and not stored on its infrastructure by default when caching or storage is not enabled. Its cache defaults to four hours when enabled; the configurable TTL can be set up to one month. Use caching for repeated renders, not as your directory’s archival policy. For persistent thumbnails, save the image to your own storage. Read the caching details and temporary screenshot URL guidance.
7. Throughput, performance, reliability, and cost
Throughput
Bulk requests share the same one-minute request bucket as regular screenshot requests. Before draining a large queue, consult ScreenshotOne’s usage endpoint and use concurrency.remaining and concurrency.reset to pace work. The documented concurrency values describe how many screenshot requests can be started in the current minute bucket, not the number of browser renders currently active. The queue guide recommends a simple in-process queue for a starter implementation and a durable queue when multiple workers or restart-safe retries are needed. Usage endpoint · Queue and bulk guidance.
Performance
- Use a viewport appropriate to the thumbnail; unnecessarily large viewport dimensions and full-page captures can increase work and output size.
- Use shared options for settings common to the whole batch, and per-request overrides only where needed.
- Use
execute: falsewhen deferred rendering fits your workflow; choose eager execution when you need API-side completion before continuing. - For repeated identical captures, consider
cache: truewith an intentionalcache_ttl. A cache hit can avoid a new render, but cache behavior is best-effort. - Only enable bulk optimization when its documented conditions fit: it requires
execute: true, and the documentation says any speedup is not guaranteed.
Reliability
Sites can be down, slow, region-dependent, or configured to reject automated traffic. Give each target a bounded timeout, record the error and attempt count, and retry transient failures with a delay and a limit. Do not retry every error forever. Preserve ready thumbnails while a refresh is pending, and use a fallback if there is no usable image yet. For high volume, make the queue durable so process restarts do not lose jobs.
Cost and retention
The dossier does not establish current ScreenshotOne commercial rates, so check the current pricing page before estimating a production budget. Estimate your expected captures per month, including initial directory population, retries, and refreshes. Caching may reduce repeated rendering when identical options are requested, but it is best-effort; do not build a cost guarantee around cache hits. Also account for the storage and image delivery costs of keeping thumbnails in your own storage.
8. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Bulk call returns an authentication error | The access key is absent, invalid, or supplied under the wrong name. | Confirm the key is passed as access_key in JSON or through a documented header. Keep it server-side and verify the environment variable is present. |
| The bulk response arrives but no image is ready | Lazy execution is in effect. | Fetch each returned screenshot URL to trigger rendering, or set execute: true and wait for completion. |
| Some directory records have no thumbnail | A particular target may fail even when the batch request itself was accepted. | Inspect each result or download status, record the failure for that listing, and retry according to a bounded policy. |
| Images look too small or have unexpected proportions | Output dimensions preserve aspect ratio rather than stretching or cropping to a fixed rectangle. | Set both maximum bounds to the card’s required box, then use CSS such as object-fit: cover if the design requires a crop. |
| Images are stale or disappear later | A returned screenshot URL is temporary, or a cache TTL expired. | Download the image and store it in your own durable storage. Use service caching only for its documented TTL and behavior. |
| Large jobs slow down or are throttled | The worker is starting requests faster than the shared one-minute bucket allows. | Check usage information and pace the queue using the remaining allowance and reset time. |
| Capture is incomplete or target refuses access | The site may load content dynamically or reject automated traffic. | Check the target URL and relevant screenshot options, use appropriate waits if needed, and retry selectively. Do not treat a failed target as a failure of every other directory entry. |
| Python or Node.js cannot reach the API | Network failure, local timeout, or incorrect endpoint. | Use HTTPS at https://api.screenshotone.com/bulk, set a realistic client timeout, and log the response status and body without logging secrets. |
9. Or skip the browser setup
ScreenshotNeo offers a website screenshot API and an MCP server. One GET request can return a screenshot or PDF. Use the direct call below for a single listing preview; for directory-scale work, build a server-side queue around requests and store durable files as needed. The ScreenshotNeo API documentation describes its request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000.
Sign up free for ScreenshotNeo.
10. FAQ
Does the bulk endpoint create the image files immediately?
Not by default. Bulk is lazy unless you set execute: true; with lazy behavior, the screenshot is rendered when its returned URL is downloaded.
Can each directory URL use different settings?
Yes. Put common defaults in options and override them in an individual request object.
Does resizing fill an exact thumbnail rectangle?
No. It preserves the screenshot’s aspect ratio and fits within the requested maximum width and height. Apply a crop in your image delivery layer if the card design needs a fixed shape.
Can I use the returned screenshot URL as permanent directory storage?
No. Treat returned screenshot URLs as temporary. Download the files to storage you control when listings need persistent previews.


