2026-03-28
8 min read
Website Screenshot API: Capture Any Page Programmatically
Taking a screenshot of a webpage sounds simple until you actually try to automate it. You need a browser, a runtime to drive it, somewhere to host it, and a way to handle the thousand things that can go wrong -- pop-ups, cookie banners, lazy-loaded images, custom fonts that haven't finished loading yet.
Most developers start with Puppeteer or Playwright running on a server somewhere. It works for the first few hundred screenshots. Then things get weird. Pages render differently. Some sites block your datacenter IP. Memory leaks crash your browser pool at 3am. You spend more time babysitting headless Chrome than building your actual product.
A website screenshot API eliminates all of that. You send a URL, you get a screenshot back. Someone else handles the browsers, the rendering, and the infrastructure. But not all screenshot APIs are built the same -- and the underlying architecture matters more than you'd think.
Why programmatic screenshots matter
Screenshots aren't just for documentation. They're a building block for a surprising number of workflows:
Visual regression testing. You deploy a frontend change. Before it hits production, you screenshot every critical page and diff it against the previous version. Pixel-level diffs catch layout breaks that unit tests miss entirely -- a button that shifted 2px, a font that didn't load, an overflow that only appears at a specific viewport width.
Competitive monitoring. Screenshot competitor pages on a schedule. Run visual diffs to detect pricing changes, redesigns, new features, or messaging shifts before your team notices them manually. We covered the broader strategy in our guide on automated competitive intelligence.
Social media preview generation. Generate OG images from live page content instead of maintaining static images. When your blog post title changes, the preview image updates automatically.
Archival and compliance. Capture pages before they change. Legal teams, journalists, and researchers need timestamped visual records of web content. The Wayback Machine can't keep up with everything.
Thumbnail generation. Search engines, directories, link aggregators, and dashboards all need visual previews of URLs. Generating these on demand beats maintaining a library of static thumbnails that go stale.
How the /screenshot endpoint works
Last Crawler's screenshot API runs on edge-native browser rendering infrastructure across 300+ locations worldwide. The basic flow is straightforward:
- You send a POST request with a URL
- A real Chrome instance at the nearest edge location loads the page
- The browser waits for the page to finish rendering (network idle, fonts loaded, lazy content visible)
- It captures the screenshot and returns it as base64-encoded PNG
Here's the simplest possible request:
bash
curl -X POST https://lastcrawler.xyz/api/screenshot \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com"
}'
The response comes back as JSON with a base64-encoded image:
json
{
"screenshot": "iVBORw0KGgoAAAANSUhEUgAA...",
"metadata": {
"url": "https://example.com",
"width": 1280,
"height": 800,
"timestamp": "2026-03-28T14:22:31Z"
}
}
Decode the base64 string and you have a PNG file. That's it.
Configuration options
The default screenshot captures the visible viewport at 1280x800. But most real use cases need more control than that.
Full page vs viewport
By default, you get the viewport -- what a user would see without scrolling. Set fullPage to true to capture the entire scrollable page:
json
{
"url": "https://example.com/pricing",
"fullPage": true
}
Full page screenshots can be tall. A typical landing page might produce an image that's 1280x8000 or more. Keep that in mind for storage and processing.
Custom dimensions
Set the viewport size to match specific devices or breakpoints:
json
{
"url": "https://example.com",
"viewport": {
"width": 1440,
"height": 900
}
}
Common sizes worth knowing: 1920x1080 (desktop), 1440x900 (laptop), 768x1024 (tablet portrait), 375x812 (iPhone), 390x844 (modern Android).
Device emulation
Emulate specific devices including proper user agent strings, device pixel ratios, and touch capabilities:
json
{
"url": "https://example.com",
"device": "iPhone 14 Pro"
}
This is particularly useful for testing responsive designs and mobile-specific layouts that only trigger on real mobile viewports.
Wait conditions
Some pages need extra time. Maybe there's a loading spinner, or content loads via WebSocket, or an animation needs to finish:
json
{
"url": "https://example.com/dashboard",
"waitFor": {
"selector": ".chart-loaded",
"timeout": 10000
}
}
You can wait for a specific CSS selector to appear, a fixed delay, or network idle (no pending requests for 500ms).
Code examples
curl
bash
curl -X POST https://lastcrawler.xyz/api/screenshot \
-H "Content-Type: application/json" \
-d '{
"url": "https://news.ycombinator.com",
"viewport": { "width": 1280, "height": 800 },
"fullPage": false
}' | jq -r '.screenshot' | base64 -d > screenshot.png
JavaScript / Node.js
javascript
const response = await fetch("https://lastcrawler.xyz/api/screenshot", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://news.ycombinator.com",
viewport: { width: 1280, height: 800 },
fullPage: false,
}),
});
const { screenshot } = await response.json();
const buffer = Buffer.from(screenshot, "base64");
fs.writeFileSync("screenshot.png", buffer);
Python
python
import requests
import base64
response = requests.post(
"https://lastcrawler.xyz/api/screenshot",
json={
"url": "https://news.ycombinator.com",
"viewport": {"width": 1280, "height": 800},
"fullPage": False,
},
)
data = response.json()
image_bytes = base64.b64decode(data["screenshot"])
with open("screenshot.png", "wb") as f:
f.write(image_bytes)
Use cases in practice
Visual regression testing
The highest-value use case for a screenshot API is catching visual bugs before users do. Here's the pattern:
- Before deploying, screenshot every critical page against the staging environment
- Compare each screenshot against the baseline (the current production version)
- Flag any diffs above a pixel threshold
- Block the deploy if unexpected visual changes are detected
javascript
// Simplified visual regression check
const pages = ["/", "/pricing", "/docs", "/login"];
for (const page of pages) {
const current = await takeScreenshot(`https://staging.example.com${page}`);
const baseline = await loadBaseline(page);
const diff = pixelmatch(current, baseline, null, 1280, 800, {
threshold: 0.1,
});
if (diff > 50) {
console.log(`Visual regression detected on ${page}: ${diff} pixels differ`);
process.exit(1);
}
}
This catches the stuff that slips through code review: z-index collisions, CSS specificity bugs, third-party script injections breaking layout, missing assets on the CDN.
Competitor monitoring
Screenshot competitor pages daily and run diffs. When something changes, you get alerted:
python
import requests
import base64
from PIL import Image
from io import BytesIO
competitors = {
"competitor_a": "https://competitor-a.com/pricing",
"competitor_b": "https://competitor-b.com/pricing",
"competitor_c": "https://competitor-c.com/features",
}
for name, url in competitors.items():
response = requests.post(
"https://lastcrawler.xyz/api/screenshot",
json={"url": url, "fullPage": True},
)
data = response.json()
today_image = base64.b64decode(data["screenshot"])
# Compare with yesterday's screenshot
yesterday = load_previous_screenshot(name)
if yesterday and images_differ(today_image, yesterday, threshold=0.05):
send_slack_alert(f"{name} changed their page: {url}")
save_screenshot(name, today_image)
Pair this with structured data extraction to pull the actual pricing numbers, and you have a complete competitive intelligence pipeline.
OG image generation
Instead of designing static OG images for every page, generate them from live content:
javascript
// Generate OG image for a blog post
const ogScreenshot = await fetch("https://lastcrawler.xyz/api/screenshot", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: `https://yoursite.com/og-template?title=${encodeURIComponent(post.title)}`,
viewport: { width: 1200, height: 630 }, // OG image dimensions
}),
});
Build a simple template page that renders your blog title, author, and branding at 1200x630. Screenshot it. Now your OG images are always in sync with your content.
Archival
Capture pages at regular intervals for a historical record:
python
from datetime import datetime
urls_to_archive = [
"https://important-government-page.gov/policy",
"https://news-site.com/developing-story",
"https://competitor.com/terms-of-service",
]
for url in urls_to_archive:
response = requests.post(
"https://lastcrawler.xyz/api/screenshot",
json={"url": url, "fullPage": True},
)
data = response.json()
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"archive/{sanitize_url(url)}_{timestamp}.png"
with open(filename, "wb") as f:
f.write(base64.b64decode(data["screenshot"]))
Thumbnail generation for directories
If you're building a site directory, search engine, or link aggregator, you need thumbnails for every URL in your index:
javascript
// Generate thumbnails for a directory listing
const sites = await db.query("SELECT url FROM sites WHERE thumbnail IS NULL LIMIT 100");
const thumbnails = await Promise.all(
sites.map(async (site) => {
const res = await fetch("https://lastcrawler.xyz/api/screenshot", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: site.url,
viewport: { width: 1280, height: 800 },
}),
});
const { screenshot } = await res.json();
return { url: site.url, thumbnail: screenshot };
})
);
// Store thumbnails and resize to display size
for (const { url, thumbnail } of thumbnails) {
await db.query("UPDATE sites SET thumbnail = $1 WHERE url = $2", [thumbnail, url]);
}
Comparison with alternatives
DIY with Puppeteer / Playwright
Running your own headless browser works until it doesn't. The problems compound:
- Infrastructure overhead. You need servers, browser pools, health checks, auto-scaling, and crash recovery. Chrome is a memory hog -- each instance uses 200-500MB.
- IP blocking. Datacenter IPs get flagged. You need proxy rotation, which adds cost and latency.
- Maintenance. Chrome updates, dependency conflicts, rendering inconsistencies across versions.
- Cold starts. Spinning up a browser instance takes 2-5 seconds. At scale, you need warm pools.
If you're taking 10 screenshots a day, Puppeteer on a VPS is fine. At 10,000 a day, you're running a browser infrastructure company on the side.
ScreenshotOne, PagePixels, Brand.dev
These are dedicated screenshot API services. They work. But they're running headless Chrome in datacenters, which means:
- Requests come from a small set of datacenter IPs that sites can flag
- Browser fingerprints may not match real user traffic
- Single region means higher latency to geographically distant targets
- No ability to combine screenshots with other extraction capabilities (structured data, markdown, links)
Last Crawler's approach
Last Crawler runs real Chrome instances on a global edge network -- the same infrastructure that serves a large share of the internet's traffic. This matters for screenshots because:
Pages actually render. Sites that block datacenter IPs, show CAPTCHAs to bots, or serve degraded content to headless browsers render normally on the edge. The request looks like a real user because it's coming from trusted infrastructure. For a deeper dive into why this matters, read our post on headless browser APIs at the edge.
Lower latency. The browser instance runs at the edge location closest to the target site. A screenshot of a Tokyo-hosted site doesn't need to round-trip to a Virginia datacenter.
Combined capabilities. The same request infrastructure supports structured JSON extraction, markdown conversion, link extraction, and full crawls. You can screenshot a page and extract its data in the same workflow without managing multiple vendors.
Why edge rendering produces better screenshots
This is the part that most screenshot API comparisons skip. Where the browser runs matters for screenshot quality -- not just speed.
Anti-bot systems treat edge traffic differently
Modern anti-bot platforms maintain databases of known datacenter IP ranges. When headless Chrome connects from AWS us-east-1, the site might serve a CAPTCHA page, a simplified layout, or block the request entirely. Your "screenshot" is a picture of a block page.
Edge-native browser rendering runs on infrastructure that websites already trust. The same network handles CDN traffic, DDoS protection, and legitimate user requests for millions of sites. Anti-bot systems don't flag this traffic because it's indistinguishable from real user traffic.
Real browser fingerprinting
Datacenter headless Chrome has a detectable fingerprint. Even with stealth plugins, there are dozens of signals that bot detection systems check -- WebGL renderer strings, navigator properties, plugin lists, canvas fingerprints.
Edge-native rendering runs full Chrome instances with fingerprints that match real browsers, because they are real browsers. No stealth patches needed.
Geographic accuracy
Some sites serve different content based on the visitor's location. Edge rendering lets the browser run near the target, producing screenshots that show what actual users in that region would see. A datacenter-only service screenshots everything from whatever region their servers sit in.
For teams building scraping infrastructure that needs to work reliably, we wrote a broader guide on choosing the right web scraping API in 2026.
FAQ
How fast is the screenshot API?
Typical response times are 2-5 seconds depending on the target page's complexity and load time. Simple static pages return in under 2 seconds. Heavy SPAs with multiple API calls might take 5-8 seconds. The edge architecture reduces network latency, but the bottleneck is usually the target page itself.
What format do screenshots come back in?
PNG by default, returned as a base64-encoded string in the JSON response. PNG is lossless, which matters for visual regression testing where you need pixel-perfect comparisons.
Can I screenshot pages that require authentication?
Yes. You can pass cookies and custom headers with the request to access authenticated pages. This is useful for screenshotting dashboards, admin panels, or any page behind a login.
How do you handle dynamic content like lazy-loaded images?
The browser waits for network idle before capturing -- meaning it waits until there are no pending network requests for 500ms. You can also specify custom wait conditions: wait for a specific CSS selector to appear, wait for a JavaScript expression to evaluate to true, or set a fixed delay.
What's the maximum page size for full-page screenshots?
Full page captures work for pages up to 16,384 pixels tall (a Chrome limitation). Most pages fall well under this. If you need to capture extremely long pages, you can take multiple viewport-sized screenshots at different scroll positions.
Can I take screenshots at different device pixel ratios (retina)?
Yes. Set the deviceScaleFactor to 2 or 3 to capture retina-quality screenshots. The output image will be 2x or 3x the viewport dimensions. This is useful for generating high-DPI thumbnails and previews.
How does this compare to using a browser extension?
Browser extensions work for manual screenshots but don't scale. You can't run an extension in a CI/CD pipeline, trigger it from a cron job, or integrate it into an automated workflow. An API gives you programmatic control -- take screenshots from any backend, any language, any environment.
Get started
The screenshot API is available at https://lastcrawler.xyz/api/screenshot. No browser infrastructure to manage, no Puppeteer configs to debug, no proxy rotation to set up. Send a URL, get a screenshot.
If you need more than screenshots -- structured data extraction, markdown conversion, or full site crawls -- it's all the same API. Check out our guides on extracting structured JSON and headless browser rendering at the edge to see what else you can build.
Last Crawler
2026-03-28