The /api/screenshot endpoint provides structured webpage screenshot capture with comprehensive metadata, page analysis, and flexible rendering options.
GET /api/screenshot
url- URL-encoded webpage to screenshot
meta=1- Return JSON metadata instead of raw imageincludeImage=1- Include base64 image in JSON (only withmeta=1)download=1- Force browser download instead of inline display
fullPage=1- Capture entire scrollable page (default: viewport only)format=jpeg|png|webp- Image format (default:jpeg)quality=<number>- Quality percentage 0-100 (default:65, applies to jpeg/webp only)selector=<css-selector>- Capture specific element instead of full pagecapture=page|selector- Explicit capture mode (auto-detected if selector provided)storage_provider=cloudflare- Upload screenshot to Cloudflare R2 storage and includes3_urlin response
profileMode=persistent- Use persistent browser profile for hard targets (default: fresh incognito context)timeoutMs=<number>- Navigation timeout in milliseconds (default:30000)
debug=1- Enable debug logging and include extra diagnostic fields in response
Returns raw image bytes with metadata in HTTP headers.
Example:
GET /api/screenshot?url=https://example.comResponse Headers:
Content-Type: image/jpeg
Content-Disposition: inline; filename="screenshot-example.com-1234567890.jpg"
X-Screenshot-Status: rendered
X-Screenshot-Blocked: 0
X-Screenshot-Title: Example%20Domain
X-Screenshot-Final-Url: https://example.com/
Returns structured metadata only, no base64 image.
Example:
GET /api/screenshot?url=https://example.com&meta=1Response:
{
"ok": true,
"status": "rendered",
"inputUrl": "https://example.com",
"finalUrl": "https://example.com/",
"title": "Example Domain",
"blocked": false,
"blockReason": null,
"warnings": [],
"renderMode": "playwright",
"timings": {
"gotoMs": 542,
"settleMs": 1002,
"screenshotMs": 15,
"totalMs": 1971
},
"pageSignals": {
"anchorCount": 1,
"imageCount": 0,
"loadedImageCount": 0,
"brokenImageCount": 0,
"videoCount": 0,
"audioCount": 0
},
"screenshot": {
"format": "jpeg",
"fullPage": false,
"width": 1200,
"height": 800,
"byteLength": 14652
}
}Returns metadata plus base64-encoded image.
Example:
GET /api/screenshot?url=https://example.com&meta=1&includeImage=1&format=pngResponse:
{
"ok": true,
"status": "rendered",
"inputUrl": "https://example.com",
"finalUrl": "https://example.com/",
"title": "Example Domain",
"blocked": false,
"blockReason": null,
"warnings": [],
"renderMode": "playwright",
"timings": { ... },
"pageSignals": { ... },
"screenshot": { ... },
"imageBase64": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}Upload screenshot to Cloudflare R2 object storage and receive S3 URL in response.
Example:
GET /api/screenshot?url=https://example.com&meta=1&storage_provider=cloudflareResponse:
{
"ok": true,
"status": "rendered",
"inputUrl": "https://example.com",
"finalUrl": "https://example.com/",
"title": "Example Domain",
"blocked": false,
"blockReason": null,
"warnings": [],
"renderMode": "playwright",
"timings": { ... },
"pageSignals": { ... },
"screenshot": { ... },
"s3_url": "https://pub-ce95034f780d447bb043921b1c273e80.r2.dev/screenshots/1710345678901.jpg"
}Environment Variables Required:
R2_ACCOUNT_ID- Cloudflare account IDR2_ACCESS_KEY_ID- R2 access key IDR2_SECRET_ACCESS_KEY- R2 secret access keyR2_BUCKET- R2 bucket nameR2_PUBLIC_BASE_URL- Public R2 domain URL (e.g.,https://pub-xxx.r2.dev)
Object Key Format:
screenshots/<timestamp>.<extension>
Notes:
- Works with all image formats (jpeg, png, webp)
- If upload fails, warning is added to response and screenshot still returns
- Can be combined with
includeImage=1to get both S3 URL and base64 image - Returns public R2 URL if
R2_PUBLIC_BASE_URLis configured, otherwise returns internal R2 endpoint URL - Public URL allows direct browser access without authentication
The status field indicates the rendering outcome:
rendered- Page rendered normally with no issues detectedpartial- Page rendered but some content appears incomplete (e.g., many images not loaded, skeletons visible)blocked- Bot challenge, captcha, access denied, or other block page detectedfailed- Navigation or screenshot capture failed
The endpoint collects DOM signals to help assess page completeness:
{
"pageSignals": {
"anchorCount": 42,
"links": [
{
"href": "https://example.com/page1"
},
{
"href": "https://example.com/page2",
"title": "Page 2 Title"
}
],
"imageCount": 15,
"loadedImageCount": 12,
"brokenImageCount": 3,
"videoCount": 1,
"audioCount": 0
}
}Link Objects:
href- The link URL (always present)title- The link title attribute (optional, only if present in HTML)
The warnings array contains human-readable issues detected:
{
"warnings": [
"Only 60% of images loaded",
"5 broken images detected",
"2 visible overlay(s) may obstruct content",
"Selector \"#missing\" not found, falling back to page screenshot"
]
}Performance breakdown in milliseconds:
{
"timings": {
"gotoMs": 542,
"settleMs": 1002,
"screenshotMs": 15,
"totalMs": 1971
}
}gotoMs- Navigation timesettleMs- Time waiting for page to settle (DOM ready, images loaded, platform-specific handling)screenshotMs- Screenshot capture timetotalMs- Total request time
Capture a specific element instead of the full page:
Example:
GET /api/screenshot?url=https://reddit.com/r/example&selector=shreddit-postIf the selector is not found, the endpoint falls back to full page capture and adds a warning.
Three image formats are supported:
jpeg(default) - Good compression, supports quality parameterpng- Lossless, larger file sizewebp- Modern format, supports quality parameter
Example:
GET /api/screenshot?url=https://example.com&format=png
GET /api/screenshot?url=https://example.com&format=webp&quality=80Uses a fresh incognito browser context for clean, reproducible results.
Example:
GET /api/screenshot?url=https://example.comUses a persistent browser profile with cookies and state. Useful for sites with aggressive bot detection.
Example:
GET /api/screenshot?url=https://reddit.com/r/example&profileMode=persistentThe endpoint includes optimized handling for specific platforms:
- Auto-dismisses cookie banners and popups
- Scrolls to trigger lazy-loaded images
- Waits for Reddit media to load
- Focuses on post container
- Extended wait times for content loading
- Scroll-based lazy load triggering
- Standard DOM content loaded wait
- Brief settle period
All errors return structured JSON:
{
"ok": false,
"error": "NAVIGATION_TIMEOUT",
"message": "Navigation timeout after 30000ms",
"inputUrl": "https://example.com"
}MISSING_URL- No URL parameter providedINVALID_URL- Malformed URLINVALID_URL_PROTOCOL- Non-HTTP/HTTPS protocolNAVIGATION_TIMEOUT- Page navigation timed outNAVIGATION_FAILED- Navigation error (e.g., DNS failure, connection refused)SCREENSHOT_CAPTURE_FAILED- Screenshot capture failedINTERNAL_ERROR- Unexpected server error
curl "http://localhost:8081/api/screenshot?url=https://example.com" \
--output screenshot.jpgcurl "http://localhost:8081/api/screenshot?url=https://example.com&fullPage=1&format=png&download=1" \
--output fullpage.pngcurl "http://localhost:8081/api/screenshot?url=https://example.com&meta=1" | jqcurl "http://localhost:8081/api/screenshot?url=https://reddit.com/r/programming/comments/abc123&selector=shreddit-post&profileMode=persistent&meta=1" | jqcurl "http://localhost:8081/api/screenshot?url=https://example.com&format=webp&quality=90&meta=1&includeImage=1" | jqcurl "http://localhost:8081/api/screenshot?url=https://example.com&meta=1&debug=1" | jqAll existing query parameters are preserved:
url- Required URLdownload=1- Force downloadfullPage=1- Full page capturemeta=1- JSON metadata modedebug=1- Debug logging
New parameters are additive and optional, ensuring existing integrations continue to work.
- Use fresh mode by default for reproducible results
- Use persistent mode only for sites with aggressive bot detection
- Request metadata without image (
meta=1withoutincludeImage=1) to reduce bandwidth - Use selector capture for specific elements to reduce screenshot size
- Check status and warnings to detect partial renders or blocks
- Monitor timings to identify slow pages
- Use appropriate format: JPEG for photos, PNG for UI/text, WebP for balance
- Fresh mode is faster (no profile loading)
- Selector capture is faster than full page
- JPEG is smaller than PNG
- Lower quality reduces file size
- Shorter timeouts fail faster
- Metadata-only responses are lightweight
The endpoint ensures proper cleanup of browser resources:
- Page closed after screenshot
- Context closed after request
- Browser closed (fresh mode only)
All cleanup happens in the finally block to prevent resource leaks.