ScreenshotAPI

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/screenshot

Captures 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:

HeaderFormat
x-api-keysk_live_your_key_here
AuthorizationBearer sk_live_your_key_here

Query Parameters

Required

ParameterTypeDescription
urlstringThe URL of the page to screenshot. Must be a valid, fully-qualified URL (including https://).

Optional

ParameterTypeDefaultDescription
widthnumber1440Viewport width in pixels. Maximum: 1920.
heightnumber900Viewport height in pixels. Maximum: 10000.
fullPagestring"false"Set to "true" to capture the entire scrollable page. Ignores the height parameter when enabled.
typestring"png"Image format: "png", "jpeg", "webp", or "pdf".
qualitynumber100Image quality from 1 to 100. Only applies to JPEG and WebP formats. Ignored for PNG.
colorSchemestringForce color scheme: "light" or "dark". Emulates the prefers-color-scheme media feature.
waitUntilstring"networkidle2"Page load event to wait for before capturing. See Wait Strategies.
waitForSelectorstringCSS selector to wait for before capturing. The screenshot is taken once this element exists in the DOM.
delaynumber0Additional delay in milliseconds after the page loads and before the screenshot is taken. Range: 030000.
blockAdsbooleanfalseEnable ad blocking. Removes ads from the page before capture.
removeCookieBannersbooleanfalseAuto-remove cookie consent dialogs before capture.
cssInjectstringInject custom CSS into the page before capture. Useful for hiding elements or overriding styles.
jsInjectstringInject custom JavaScript into the page before capture. Runs after the page loads but before the screenshot is taken.
stealthModebooleanfalseEnable anti-bot-detection mode. Masks headless browser fingerprints to avoid being blocked by bot-detection systems.
devicePixelRationumber1Device pixel ratio for Retina/HiDPI captures. Accepted values: 1, 2, or 3. Higher values produce larger, sharper images.
timezonestringTimezone emulation (e.g. "America/New_York"). Overrides the browser's default timezone for the capture.
localestringLocale and Accept-Language emulation (e.g. "en-US"). Controls the language used by the browser during the capture.
cacheTtlnumber0Response 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.
preloadFontsbooleanfalsePreload all Google Fonts on the page before capture. Ensures accurate font rendering by explicitly loading every font discovered in the page's stylesheets.
removeElementsstringComma-separated CSS selectors for elements to remove before capture. Example: ".popup, #banner, .newsletter-signup". Each matching element is removed from the DOM.
removePopupsbooleanfalseAutomatically 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.
mockupDevicestringWrap the screenshot in a device frame. Values: "browser" (macOS chrome), "iphone" (iPhone with Dynamic Island), "macbook" (MacBook with bezel). Output is always PNG.
geoLatitudenumberLatitude for browser geolocation override. Must be used together with geoLongitude.
geoLongitudenumberLongitude for browser geolocation override. Must be used together with geoLatitude.
geoAccuracynumber100Accuracy in meters for the geolocation override. Only applies when geoLatitude and geoLongitude are set.

POST-only Body Parameters

ParameterTypeDescription
htmlstringRaw 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":

ValueDescription
loadWaits for the load event (all resources including images loaded).
domcontentloadedWaits for the DOMContentLoaded event (HTML parsed, but images/stylesheets may still be loading).
networkidle0Waits until there are no more than 0 network connections for 500ms. Best for fully static content.
networkidle2Waits 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:

HeaderTypeDescription
Content-Typestringimage/png, image/jpeg, image/webp, or application/pdf
x-credits-remainingstringYour remaining credit balance
x-screenshot-idstringUnique identifier for this screenshot
x-duration-msstringTime taken to capture the screenshot in milliseconds

Body: Binary image data.

Error Responses

StatuscodeRetryableDescription
400UNSAFE_TARGETNoThe target is localhost, an internal hostname, or a private/reserved IP address. These are never captured.
400TARGET_UNRESOLVABLENoDNS answered definitively that the hostname has no address. Check the spelling and that it resolves publicly.
401NoNo API key provided ({"error": "API key required"}).
402NoMonthly plan limit reached and no credits remaining. See Usage Limits & Quota.
403NoAPI key is invalid or revoked.
500SCREENSHOT_FAILEDNoThe capture itself failed — for example the page never finished loading, or it blocked the browser.
503TARGET_RESOLUTION_UNAVAILABLEYesWe 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.png
const 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.webp
const 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.png

Custom 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.png

Wait 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.png

Rate 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 waitForSelector for 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.

On this page