Screenshot API
Complete reference for the GET /api/v1/screenshot endpoint — capture any web page as an image.
Endpoint
GET /api/v1/screenshot
POST /api/v1/screenshotCaptures a screenshot of the specified URL (or raw HTML via POST) and returns the binary image data directly in the response body. Requires API key authentication. Use POST when sending raw HTML via the html body parameter.
Don't have a key yet? There is also a keyless endpoint —
GET /api/v1/public/screenshot — that
returns an image with no API key and no signup, at 8 requests/minute with
tighter caps and no PDF support. Good for a quick test or a low-volume embed;
this authenticated endpoint is the one to build on.
Need the render itself in front-end HTML? On the Growth plan and above a
signed URL authorises one exact GET for a
limited time, so an <img src> can carry proof of authorisation instead of
your API key.
Authentication
Include your API key in one of these headers:
| Header | Format |
|---|---|
x-api-key | sk_live_your_key_here |
Authorization | Bearer sk_live_your_key_here |
Query Parameters
Required
| Parameter | Type | Description |
|---|---|---|
url | string | The URL of the page to screenshot. Must be a valid, fully-qualified URL (including https://). |
Optional
| Parameter | Type | Default | Description |
|---|---|---|---|
width | number | 1440 | Viewport width in pixels. Maximum: 1920. |
height | number | 900 | Viewport height in pixels. Maximum: 10000. |
fullPage | string | "false" | Set to "true" to capture the entire scrollable page. Ignores the height parameter when enabled. |
type | string | "png" | Image format: "png", "jpeg", "webp", or "pdf". |
quality | number | 100 | Image quality from 1 to 100. Only applies to JPEG and WebP formats. Ignored for PNG. |
colorScheme | string | — | Force color scheme: "light" or "dark". Emulates the prefers-color-scheme media feature. |
waitUntil | string | "networkidle2" | Page load event to wait for before capturing. See Wait Strategies. |
waitForSelector | string | — | CSS selector to wait for before capturing. The screenshot is taken once this element exists in the DOM. |
delay | number | 0 | Additional delay in milliseconds after the page loads and before the screenshot is taken. Range: 0–30000. |
blockAds | boolean | false | Enable ad blocking. Removes ads from the page before capture. |
removeCookieBanners | boolean | false | Auto-remove cookie consent dialogs before capture. |
cssInject | string | — | Inject custom CSS into the page before capture. Useful for hiding elements or overriding styles. |
jsInject | string | — | Inject custom JavaScript into the page before capture. Runs after the page loads but before the screenshot is taken. |
stealthMode | boolean | false | Enable anti-bot-detection mode. Masks headless browser fingerprints to avoid being blocked by bot-detection systems. |
devicePixelRatio | number | 1 | Device pixel ratio for Retina/HiDPI captures. Accepted values: 1, 2, or 3. Higher values produce larger, sharper images. |
timezone | string | — | Timezone emulation (e.g. "America/New_York"). Overrides the browser's default timezone for the capture. |
locale | string | — | Locale and Accept-Language emulation (e.g. "en-US"). Controls the language used by the browser during the capture. |
cacheTtl | number | 0 | Response cache TTL in seconds. When set to a value greater than 0, identical requests within the TTL window return a cached response. Set to 0 to disable caching. |
preloadFonts | boolean | false | Preload all Google Fonts on the page before capture. Ensures accurate font rendering by explicitly loading every font discovered in the page's stylesheets. |
removeElements | string | — | Comma-separated CSS selectors for elements to remove before capture. Example: ".popup, #banner, .newsletter-signup". Each matching element is removed from the DOM. |
removePopups | boolean | false | Automatically remove common popups, modals, overlays, and interstitials before capture. Targets elements with position: fixed/sticky and high z-index that match popup-like class names. |
mockupDevice | string | — | Wrap the screenshot in a device frame. Values: "browser" (macOS chrome), "iphone" (iPhone with Dynamic Island), "macbook" (MacBook with bezel). Output is always PNG. |
geoLatitude | number | — | Latitude for browser geolocation override. Must be used together with geoLongitude. |
geoLongitude | number | — | Longitude for browser geolocation override. Must be used together with geoLatitude. |
geoAccuracy | number | 100 | Accuracy in meters for the geolocation override. Only applies when geoLatitude and geoLongitude are set. |
POST-only Body Parameters
| Parameter | Type | Description |
|---|---|---|
html | string | Raw HTML string to render instead of navigating to a URL. When provided, the url parameter is ignored. Must use POST method with a JSON body. |
Wait Strategies
The waitUntil parameter controls when the page is considered "loaded":
| Value | Description |
|---|---|
load | Waits for the load event (all resources including images loaded). |
domcontentloaded | Waits for the DOMContentLoaded event (HTML parsed, but images/stylesheets may still be loading). |
networkidle0 | Waits until there are no more than 0 network connections for 500ms. Best for fully static content. |
networkidle2 | Waits until there are no more than 2 network connections for 500ms. Default. Good balance for most pages, tolerates long-polling or analytics connections. |
For single-page applications (SPAs) that load content dynamically, combine waitUntil: "networkidle2" with waitForSelector targeting a key content element, or add a delay.
Response
Success (200)
Headers:
| Header | Type | Description |
|---|---|---|
Content-Type | string | image/png, image/jpeg, image/webp, or application/pdf |
x-credits-remaining | string | Your remaining credit balance |
x-screenshot-id | string | Unique identifier for this screenshot |
x-duration-ms | string | Time taken to capture the screenshot in milliseconds |
Body: Binary image data.
Error Responses
| Status | code | Retryable | Description |
|---|---|---|---|
400 | UNSAFE_TARGET | No | The target is localhost, an internal hostname, or a private/reserved IP address. These are never captured. |
400 | TARGET_UNRESOLVABLE | No | DNS answered definitively that the hostname has no address. Check the spelling and that it resolves publicly. |
401 | — | No | No API key provided ({"error": "API key required"}). |
402 | — | No | Monthly plan limit reached and no credits remaining. See Usage Limits & Quota. |
403 | — | No | API key is invalid or revoked. |
500 | SCREENSHOT_FAILED | No | The capture itself failed — for example the page never finished loading, or it blocked the browser. |
503 | TARGET_RESOLUTION_UNAVAILABLE | Yes | We could not look up the hostname right now. This is a fault on our side, not a problem with your URL. |
Every render failure returns a stable code you can branch on, plus retryable, a human-readable
message, a suggested fix, and docsUrl:
{
"error": "Temporarily unable to resolve the target",
"code": "TARGET_RESOLUTION_UNAVAILABLE",
"message": "We could not look up that hostname right now. This is a temporary fault on our side, not a problem with your URL.",
"fix": "Retry the same request after 2 seconds. Treat 503 as retryable in your integration.",
"retryable": true,
"retryAfterSeconds": 2,
"docsUrl": "https://screenshotapi.to/docs/api/screenshot"
}Failed captures never consume a screenshot from your allowance or a credit.
Retrying
503 responses also carry a Retry-After header. Branch on retryable rather than on the message
text, and retry the identical request:
async function capture(url, key, attempts = 3) {
for (let attempt = 1; attempt <= attempts; attempt++) {
const response = await fetch(url, { headers: { 'x-api-key': key } })
if (response.ok) return Buffer.from(await response.arrayBuffer())
const body = await response.json()
if (!body.retryable || attempt === attempts) throw new Error(body.message)
const waitSeconds = Number(response.headers.get('retry-after') ?? body.retryAfterSeconds ?? 2)
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000 * attempt))
}
}Usage Limits & Quota
Every account includes 200 free screenshots per month. Each successful capture consumes one screenshot from your plan allowance first, then from any credit packs you've purchased.
Your included allowance is per billing period, and it resets when that period rolls over — on the first of the month for free accounts, and on your subscription anniversary for paid plans. Credit packs are separate: they never expire and are only drawn on once the included allowance for the period is used up.
When you've used your plan's allowance for the current period and have no credits left, the API returns 402 Payment Required and does not process the request (no credit is deducted):
{
"error": "Insufficient quota",
"message": "You've reached your plan's monthly screenshot limit. Your included allowance resets on 2026-08-01. Upgrade your plan or buy a credit pack to keep capturing now: https://screenshotapi.to/dashboard/billing",
"remainingInPlan": 0,
"creditBalance": 0,
"quotaResetsAt": "2026-08-01T00:00:00.000Z",
"upgradeUrl": "https://screenshotapi.to/dashboard/billing"
}quotaResetsAt is the ISO timestamp at which your included allowance returns, so an integration can schedule a retry instead of failing indefinitely. It is null only when the account holds no entitled plan.
To restore access immediately rather than waiting for the reset, either upgrade your plan for a higher allowance (Starter 5,000/mo, Growth 25,000/mo, Scale 100,000/mo) or buy a one-time credit pack — both are self-serve at the upgradeUrl returned in the response. Paid plans continue serving screenshots past their allowance as metered overage rather than returning 402.
Handle 402 in your integration by surfacing message (or opening upgradeUrl) so the account owner can restore service in one click, and use quotaResetsAt to decide whether waiting is an option:
const response = await fetch(url, { headers: { 'x-api-key': key } })
if (response.status === 402) {
const { message, upgradeUrl, quotaResetsAt } = await response.json()
console.error(message) // includes the reset date and the self-serve upgrade link
// e.g. notify the account owner, open `upgradeUrl`, or requeue after `quotaResetsAt`
}Examples
Basic Screenshot
curl "https://screenshotapi.to/api/v1/screenshot?url=https://example.com" \
-H "x-api-key: sk_live_your_key_here" \
--output screenshot.pngconst response = await fetch(
'https://screenshotapi.to/api/v1/screenshot?url=https://example.com',
{ headers: { 'x-api-key': 'sk_live_your_key_here' } }
)
if (!response.ok) {
const error = await response.json()
throw new Error(error.message)
}
const buffer = Buffer.from(await response.arrayBuffer())
await fs.promises.writeFile('screenshot.png', buffer)import requests
response = requests.get(
"https://screenshotapi.to/api/v1/screenshot",
params={"url": "https://example.com"},
headers={"x-api-key": "sk_live_your_key_here"}
)
response.raise_for_status()
with open("screenshot.png", "wb") as f:
f.write(response.content)req, _ := http.NewRequest("GET",
"https://screenshotapi.to/api/v1/screenshot?url=https://example.com", nil)
req.Header.Set("x-api-key", "sk_live_your_key_here")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
file, _ := os.Create("screenshot.png")
defer file.Close()
io.Copy(file, resp.Body)require "net/http"
require "uri"
uri = URI("https://screenshotapi.to/api/v1/screenshot?url=https://example.com")
req = Net::HTTP::Get.new(uri)
req["x-api-key"] = "sk_live_your_key_here"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http|
http.request(req)
}
File.binwrite("screenshot.png", response.body)$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://screenshotapi.to/api/v1/screenshot?url=https://example.com",
CURLOPT_HTTPHEADER => ["x-api-key: sk_live_your_key_here"],
CURLOPT_RETURNTRANSFER => true,
]);
$image = curl_exec($ch);
curl_close($ch);
file_put_contents("screenshot.png", $image);Full-Page Screenshot
Capture the entire scrollable page as a single tall image:
curl "https://screenshotapi.to/api/v1/screenshot?url=https://example.com&fullPage=true&type=webp&quality=90" \
-H "x-api-key: sk_live_your_key_here" \
--output fullpage.webpconst params = new URLSearchParams({
url: 'https://example.com',
fullPage: 'true',
type: 'webp',
quality: '90'
})
const response = await fetch(
`https://screenshotapi.to/api/v1/screenshot?${params}`,
{ headers: { 'x-api-key': 'sk_live_your_key_here' } }
)
const buffer = Buffer.from(await response.arrayBuffer())
await fs.promises.writeFile('fullpage.webp', buffer)import requests
response = requests.get(
"https://screenshotapi.to/api/v1/screenshot",
params={
"url": "https://example.com",
"fullPage": "true",
"type": "webp",
"quality": "90"
},
headers={"x-api-key": "sk_live_your_key_here"}
)
with open("fullpage.webp", "wb") as f:
f.write(response.content)req, _ := http.NewRequest("GET",
"https://screenshotapi.to/api/v1/screenshot?url=https://example.com&fullPage=true&type=webp&quality=90", nil)
req.Header.Set("x-api-key", "sk_live_your_key_here")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
file, _ := os.Create("fullpage.webp")
defer file.Close()
io.Copy(file, resp.Body)require "net/http"
require "uri"
uri = URI("https://screenshotapi.to/api/v1/screenshot?url=https://example.com&fullPage=true&type=webp&quality=90")
req = Net::HTTP::Get.new(uri)
req["x-api-key"] = "sk_live_your_key_here"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http|
http.request(req)
}
File.binwrite("fullpage.webp", response.body)$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://screenshotapi.to/api/v1/screenshot?url=https://example.com&fullPage=true&type=webp&quality=90",
CURLOPT_HTTPHEADER => ["x-api-key: sk_live_your_key_here"],
CURLOPT_RETURNTRANSFER => true,
]);
$image = curl_exec($ch);
curl_close($ch);
file_put_contents("fullpage.webp", $image);Dark Mode Screenshot
Force dark mode rendering on any page that supports prefers-color-scheme:
curl "https://screenshotapi.to/api/v1/screenshot?url=https://example.com&colorScheme=dark" \
-H "x-api-key: sk_live_your_key_here" \
--output dark.pngCustom Viewport Size
Capture at a mobile viewport size:
curl "https://screenshotapi.to/api/v1/screenshot?url=https://example.com&width=390&height=844" \
-H "x-api-key: sk_live_your_key_here" \
--output mobile.pngWait for Dynamic Content
Wait for a specific element to appear before capturing:
curl "https://screenshotapi.to/api/v1/screenshot?url=https://example.com&waitForSelector=.hero-loaded&delay=500" \
-H "x-api-key: sk_live_your_key_here" \
--output dynamic.pngRate Limits
There are currently no enforced rate limits. However, screenshots are generated sequentially per API key. For maximum throughput, use multiple API keys or send requests in parallel — each request will be queued and processed as resources become available.
Best Practices
- Use
networkidle2(the default) for most pages. It handles analytics scripts and long-polling gracefully. - Use
waitForSelectorfor SPAs where content loads asynchronously after the initial page load. - Use WebP format for smaller file sizes when quality is acceptable (typically 30–50% smaller than PNG).
- Set appropriate viewport dimensions — the default 1440×900 works for most desktop pages, but use 390×844 for mobile screenshots.
- Add a small
delay(200–500ms) for pages with CSS animations or transitions that should complete before capture.
Credits
Understand the credit-based pricing system, purchase credit packs, and configure auto top-up.
Free Screenshot URL (No API Key)
A keyless screenshot endpoint — GET /api/v1/public/screenshot?url=... returns a PNG, JPEG, or WebP image with no API key and no signup. Rate limits, parameters, caps, and embed examples.