API examples

Production-ready recipes, as URLs, cURL, and SDK calls.

Copy-paste recipes for screenshots, PDFs, content extraction, batch jobs, visual diffs, and async webhooks. Flip the switch to see a direct URL, raw HTTP request, or a typed Node or Java SDK call for the same result — or hand the whole thing to an MCP agent.

Base URL https://api.snapshotflow.com · authenticate with your API key from the dashboard.

0

Set up once

Shell

# Grab your key from the dashboard (Account → API Keys)
export API_KEY="e2e_your_key_here"

# Every request authenticates with the X-Api-Key header.
# (You can also pass ?api_key=... in the query string.)
curl "https://api.snapshotflow.com/screenshot?url=https://example.com" \
      -H "X-Api-Key: $API_KEY" -o page.png

Direct URL

https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&format=png&url=https://apple.com

Install

npm install snapshotflow   # requires Node.js >= 18

Create the client (reused by every example below)

import { SnapshotFlow, TakeOptions } from "snapshotflow";

const client = new SnapshotFlow({
  apiKey: process.env.SNAPSHOTFLOW_API_KEY,
  baseUrl: "https://api.snapshotflow.com",
  // timeoutMs and maxRetries are optional
});

Install — Maven pom.xml

<dependency>
    <groupId>com.snapshotflow</groupId>
    <artifactId>snapshotflow-java</artifactId>
    <version>1.0.0</version>
</dependency>

<!-- Gradle: implementation 'com.snapshotflow:snapshotflow-java:1.0.0' -->

Create the client (reused by every example below)

import com.snapshotflow.SnapshotFlow;
import com.snapshotflow.TakeOptions;
import com.snapshotflow.ScreenshotResult;
import java.nio.file.Path;

SnapshotFlow client = SnapshotFlow.builder()
    .apiKey(System.getenv("SNAPSHOTFLOW_API_KEY"))
    .baseUrl("https://api.snapshotflow.com")
    // .timeout(Duration.ofSeconds(60)) and .maxRetries(2) are optional
    .build();

Connect once — Claude, Cursor, Codex, or any MCP client

{
  "mcpServers": {
    "snapshotflow": {
      "url": "https://api.snapshotflow.com/mcp"
    }
  }
}

What the agent gets

# Auth is OAuth 2.0 — your MCP client opens a browser to sign in.
# No API key to manage. The agent receives five tools:
#
#   screenshot        capture a URL or base64 HTML
#   batch_screenshot  capture 1-10 URLs in one request
#   visual_diff       pixel-compare two URLs
#   extract_text      read a page as markdown/text (no image)
#   check_job         follow an async screenshot job

Capture basics

The everyday screenshot calls.

Basic screenshot (PNG)

The simplest request — one URL in, a binary PNG out.

url
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com" \
  -H "X-Api-Key: $API_KEY" -o stripe.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com
const shot = await client.take({ url: "https://stripe.com" });
await shot.save("stripe.png");
console.log("cached:", shot.cached, "type:", shot.contentType);
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com"));
shot.save(Path.of("stripe.png"));
System.out.println("cached=" + shot.cached() + " type=" + shot.contentType());
# Ask your agent:
"Take a screenshot of https://stripe.com and describe
 what's above the fold."

# Tool call the agent makes:
screenshot { "url": "https://stripe.com" }

Full-page JPEG with quality

Capture the whole scrollable page as a compressed JPEG.

full_pageformatquality
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&full_page=true&format=jpeg&quality=85" \
  -H "X-Api-Key: $API_KEY" -o full.jpg
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&full_page=true&format=jpeg&quality=85
const shot = await client.take(
  TakeOptions.url("https://stripe.com")
    .fullPage(true)
    .format("jpeg")
    .quality(85),
);
await shot.save("full.jpg");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com")
        .fullPage(true)
        .format("jpeg")
        .quality(85));
shot.save(Path.of("full.jpg"));
# Ask your agent:
"Capture the entire stripe.com page as a JPEG."

# Tool call the agent makes:
screenshot {
  "url": "https://stripe.com",
  "full_page": true,
  "format": "jpeg"
}
# quality tuning is API/SDK-only

Retina & custom viewport

A 1440×900 viewport at 2× device scale for crisp, high-DPI output.

widthheightdevice_scale_factor
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&width=1440&height=900&device_scale_factor=2" \
  -H "X-Api-Key: $API_KEY" -o retina.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&width=1440&height=900&device_scale_factor=2
const shot = await client.take({
  url: "https://stripe.com",
  width: 1440,
  height: 900,
  deviceScaleFactor: 2,
});
await shot.save("retina.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com")
        .width(1440)
        .height(900)
        .deviceScaleFactor(2));
shot.save(Path.of("retina.png"));
# Ask your agent:
"Screenshot stripe.com at a 1440x900 desktop viewport."

# Tool call the agent makes:
screenshot {
  "url": "https://stripe.com",
  "width": 1440,
  "height": 900
}
# device_scale_factor (retina) is API/SDK-only

One element only (selector)

Crop the capture to a single CSS selector — great for cards or tables.

selectoromit_background
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&selector=.PricingTable&omit_background=true" \
  -H "X-Api-Key: $API_KEY" -o pricing.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&selector=main&omit_background=true
const shot = await client.take({
  url: "https://stripe.com",
  selector: ".PricingTable",
  omitBackground: true,   // transparent PNG
});
await shot.save("pricing.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com")
        .selector(".PricingTable")
        .omitBackground(true));   // transparent PNG
shot.save(Path.of("pricing.png"));
# Cropping to one selector is API/SDK-only. Over MCP the
# closest recipe is waiting for the element, then capturing:
screenshot {
  "url": "https://stripe.com",
  "wait_for_selector": ".PricingTable"
}
# The agent then reads or crops the returned image itself.

Mobile viewport + dark mode

Emulate a mobile device and force the dark color scheme.

viewport_mobiledark_mode
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&viewport_mobile=true&dark_mode=true" \
  -H "X-Api-Key: $API_KEY" -o mobile-dark.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&viewport_mobile=true&dark_mode=true
const shot = await client.take({
  url: "https://stripe.com",
  viewportMobile: true,
  darkMode: true,
});
await shot.save("mobile-dark.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com")
        .viewportMobile(true)
        .darkMode(true));
shot.save(Path.of("mobile-dark.png"));
# Ask your agent:
"Screenshot stripe.com in dark mode at a phone-sized viewport."

# Tool call the agent makes:
screenshot {
  "url": "https://stripe.com",
  "width": 390,
  "height": 844,
  "dark_mode": true
}

Render raw HTML

Screenshot an HTML string instead of a URL — pass it base64-encoded.

html
HTML=$(printf '<h1>Hello</h1>' | base64)
curl "https://api.snapshotflow.com/screenshot?html=$HTML&format=png" \
  -H "X-Api-Key: $API_KEY" -o html.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&html=PGgxPkhlbGxvPC9oMT4%3D&format=png
// The SDK base64-encodes the HTML for you.
const shot = await client.take({
  html: "<h1>Hello</h1>",
  format: "png",
});
await shot.save("html.png");
// The SDK base64-encodes the HTML for you.
ScreenshotResult shot = client.take(
    TakeOptions.html("<h1>Hello</h1>")
        .format("png"));
shot.save(Path.of("html.png"));
# Ask your agent:
"Render this HTML snippet and show me how it looks."

# Tool call the agent makes (HTML is base64-encoded):
screenshot {
  "html": "PGgxPkhlbGxvPC9oMT4=",
  "format": "png"
}

Clean & control

Block noise, wait for content, inject your own code.

Block ads, trackers & cookie banners

Strip the clutter and hide your own selectors for a clean capture.

block_adsblock_trackersblock_cookie_bannershide_selectors
curl "https://api.snapshotflow.com/screenshot?url=https://news.ycombinator.com&block_ads=true&block_trackers=true&block_cookie_banners=true&hide_selectors=.intercom-launcher" \
  -H "X-Api-Key: $API_KEY" -o clean.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&block_ads=true&block_trackers=true&block_cookie_banners=true&hide_selectors=.intercom-launcher
const shot = await client.take({
  url: "https://news.ycombinator.com",
  blockAds: true,
  blockTrackers: true,
  blockCookieBanners: true,
  hideSelectors: [".intercom-launcher"],
});
await shot.save("clean.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://news.ycombinator.com")
        .blockAds(true)
        .blockTrackers(true)
        .blockCookieBanners(true)
        .hideSelectors(".intercom-launcher"));
shot.save(Path.of("clean.png"));
# Ask your agent:
"Screenshot news.ycombinator.com without ads or cookie banners."

# Tool call the agent makes:
screenshot {
  "url": "https://news.ycombinator.com",
  "block_ads": true,
  "block_cookie_banners": true
}
# block_trackers and hide_selectors are API/SDK-only

Wait for a SPA to render

Hold the capture until an element appears and the network settles.

wait_for_selectorwait_untildelay
curl "https://api.snapshotflow.com/screenshot?url=https://app.example.com&wait_for_selector=.dashboard-loaded&wait_until=networkidle0&delay=500" \
  -H "X-Api-Key: $API_KEY" -o spa.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&wait_for_selector=main&wait_until=networkidle0&delay=500
const shot = await client.take({
  url: "https://app.example.com",
  waitForSelector: ".dashboard-loaded",
  waitUntil: "networkidle0",
  delay: 500,
});
await shot.save("spa.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://app.example.com")
        .waitForSelector(".dashboard-loaded")
        .waitUntil("networkidle0")
        .delay(500));
shot.save(Path.of("spa.png"));
# Ask your agent:
"Screenshot app.example.com once the dashboard has rendered."

# Tool call the agent makes:
screenshot {
  "url": "https://app.example.com",
  "wait_for_selector": ".dashboard-loaded",
  "delay": 500
}
# wait_until strategies are API/SDK-only

Authenticated page (headers + cookies)

Send auth headers and session cookies to capture a logged-in view.

headerscookies
curl -G "https://api.snapshotflow.com/screenshot" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "url=https://app.example.com/billing" \
  --data-urlencode 'headers={"Authorization":"Bearer token123"}' \
  --data-urlencode 'cookies=[{"name":"session","value":"abc","domain":"app.example.com"}]' \
  -o billing.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com/shop&headers=%7B%22Authorization%22%3A%22Bearer%20token123%22%7D&cookies=%5B%7B%22name%22%3A%22session%22%2C%22value%22%3A%22abc%22%2C%22domain%22%3A%22apple.com%22%7D%5D
const shot = await client.take({
  url: "https://app.example.com/billing",
  headers: { Authorization: "Bearer token123" },
  cookies: [
    { name: "session", value: "abc", domain: "app.example.com" },
  ],
});
await shot.save("billing.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://app.example.com/billing")
        .headers(Map.of("Authorization", "Bearer token123"))
        .cookie("session", "abc", "app.example.com"));
shot.save(Path.of("billing.png"));
# Custom auth headers and session cookies are not exposed
# over MCP — captures of logged-in pages need the API or SDKs.
#
# Agents can still work with any public page, e.g.:
screenshot { "url": "https://example.com/pricing" }

Inject CSS/JS & click first

Patch the page, dismiss a modal, then capture the result.

stylesscriptsclick
curl -G "https://api.snapshotflow.com/screenshot" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "styles=.promo-bar{display:none}" \
  --data-urlencode "scripts=document.title='Snap'" \
  --data-urlencode "click=#accept-modal .close" \
  -o patched.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&styles=.promo-bar%7Bdisplay%3Anone%7D&scripts=document.title%3D%27Snap%27&click=button
const shot = await client.take({
  url: "https://example.com",
  styles: ".promo-bar{display:none}",
  scripts: "document.title='Snap'",
  click: "#accept-modal .close",
});
await shot.save("patched.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .styles(".promo-bar{display:none}")
        .scripts("document.title='Snap'")
        .click("#accept-modal .close"));
shot.save(Path.of("patched.png"));
# Injecting CSS/JS and clicking elements are API/SDK-only.
# Over MCP, blocking often removes the noise you'd patch away:
screenshot {
  "url": "https://example.com",
  "block_ads": true,
  "block_cookie_banners": true
}

Geolocation & timezone

Render the page as if the browser were in another location.

timezonegeolocation_latitudegeolocation_longitude
curl "https://api.snapshotflow.com/screenshot?url=https://example.com&timezone=Europe/Paris&geolocation_latitude=48.8566&geolocation_longitude=2.3522&geolocation_accuracy=50" \
  -H "X-Api-Key: $API_KEY" -o paris.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&timezone=Europe/Paris&geolocation_latitude=48.8566&geolocation_longitude=2.3522&geolocation_accuracy=50
// The fluent builder exposes .geolocation(lat, lng, accuracy).
const shot = await client.take(
  TakeOptions.url("https://example.com")
    .timezone("Europe/Paris")
    .geolocation(48.8566, 2.3522, 50),
);
await shot.save("paris.png");

// Plain-object equivalent uses flat keys:
// { geolocationLatitude: 48.8566, geolocationLongitude: 2.3522,
//   geolocationAccuracy: 50, timezone: "Europe/Paris" }
// The builder exposes .geolocation(lat, lng, accuracy).
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .timezone("Europe/Paris")
        .geolocation(48.8566, 2.3522, 50));
shot.save(Path.of("paris.png"));
# Timezone and geolocation emulation are API/SDK-only —
# they are not part of the MCP screenshot tool's schema.
#
# Point your agent at the docs for these parameters:
# https://snapshotflow.com/docs

Generate an <img> URL

Build a ready-to-embed URL with no request — the key goes in the query string, so use only where exposing it is OK.

api_keyfull_page
<!-- Embed directly in HTML -->
<img
  src="https://api.snapshotflow.com/screenshot?url=https://stripe.com&full_page=true&api_key=e2e_your_key_here"
  alt="stripe.com">
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&full_page=true
// No network call — just builds the URL string.
const imgUrl = client.generateUrl({
  url: "https://stripe.com",
  fullPage: true,
});
// => use directly in an <img src>
// No network call — just builds the URL string.
String imgUrl = client.generateUrl(
    TakeOptions.url("https://stripe.com").fullPage(true));
// => use directly in an <img src>
# MCP tools return the image straight to the agent's context,
# so there is no URL to embed. To build <img> URLs, use the
# API key + query string (URL tab) or generateUrl() in the SDKs.
#
# Agents that need to *look* at a page just call:
screenshot { "url": "https://stripe.com", "full_page": true }

Advanced rendering

Crop, resize, emulate, and fine-tune the request.

Crop a fixed region (clip)

Capture an exact rectangle by coordinates — perfect for OG cards.

clip_xclip_yclip_widthclip_height
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&clip_x=0&clip_y=0&clip_width=1200&clip_height=630" \
  -H "X-Api-Key: $API_KEY" -o og-card.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&clip_x=0&clip_y=0&clip_width=1200&clip_height=630
// Builder: .clip(x, y, width, height)
const shot = await client.take(
  TakeOptions.url("https://stripe.com").clip(0, 0, 1200, 630),
);
await shot.save("og-card.png");

// Plain-object equivalent:
// { clipX: 0, clipY: 0, clipWidth: 1200, clipHeight: 630 }
// Builder: .clip(x, y, width, height)
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com").clip(0, 0, 1200, 630));
shot.save(Path.of("og-card.png"));
# Pixel-exact clipping is API/SDK-only. Over MCP, set the
# viewport to the region size and skip full_page:
screenshot {
  "url": "https://stripe.com",
  "width": 1200,
  "height": 630
}

Resize the output

Render at a large viewport, then downscale the final image to exact pixels.

image_widthimage_height
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&width=1440&height=900&image_width=600&image_height=375" \
  -H "X-Api-Key: $API_KEY" -o thumb.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&width=1440&height=900&image_width=600&image_height=375
const shot = await client.take({
  url: "https://stripe.com",
  width: 1440,
  height: 900,
  imageWidth: 600,    // downscale after capture
  imageHeight: 375,
});
await shot.save("thumb.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://stripe.com")
        .width(1440)
        .height(900)
        .imageWidth(600)    // downscale after capture
        .imageHeight(375));
shot.save(Path.of("thumb.png"));
# Post-capture downscaling (image_width/height) is API/SDK-only.
# Agents usually don't need thumbnails — a smaller viewport
# keeps the returned image light for the context window:
screenshot {
  "url": "https://stripe.com",
  "width": 800,
  "height": 500
}

Reduced motion & print media

Freeze animations and emulate @media print styles.

reduced_motionmedia_type
curl "https://api.snapshotflow.com/screenshot?url=https://example.com&reduced_motion=true&media_type=print" \
  -H "X-Api-Key: $API_KEY" -o print.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&reduced_motion=true&media_type=print
const shot = await client.take({
  url: "https://example.com",
  reducedMotion: true,
  mediaType: "print",   // "screen" | "print"
});
await shot.save("print.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .reducedMotion(true)
        .mediaType("print"));   // "screen" | "print"
shot.save(Path.of("print.png"));
# reduced_motion and media_type emulation are API/SDK-only.
# For animation-heavy pages, a short delay before capture
# usually settles the layout:
screenshot {
  "url": "https://example.com",
  "delay": 1000
}

Custom user agent

Identify your crawler or trigger UA-specific rendering.

user_agent
curl -G "https://api.snapshotflow.com/screenshot" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "user_agent=SnapshotFlowBot/1.0 (+https://example.com/bot)" \
  -o ua.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&user_agent=SnapshotFlowBot/1.0
const shot = await client.take({
  url: "https://example.com",
  userAgent: "SnapshotFlowBot/1.0 (+https://example.com/bot)",
});
await shot.save("ua.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .userAgent("SnapshotFlowBot/1.0 (+https://example.com/bot)"));
shot.save(Path.of("ua.png"));
# Custom user agents are API/SDK-only — MCP captures use
# the backend's default browser UA. The standard call:
screenshot { "url": "https://example.com" }

Block specific requests

Drop requests matching URL patterns — heavy scripts, beacons, third-party widgets.

block_requests
curl -G "https://api.snapshotflow.com/screenshot" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "block_requests=*.googletagmanager.com,*/analytics.js,*.hotjar.com" \
  -o light.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&block_requests=*.googletagmanager.com,*/analytics.js,*.hotjar.com
const shot = await client.take({
  url: "https://example.com",
  blockRequests: [
    "*.googletagmanager.com",
    "*/analytics.js",
    "*.hotjar.com",
  ],
});
await shot.save("light.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .blockRequests(
            "*.googletagmanager.com",
            "*/analytics.js",
            "*.hotjar.com"));
shot.save(Path.of("light.png"));
# Per-pattern request blocking is API/SDK-only. Over MCP,
# the built-in blockers cover the usual suspects:
screenshot {
  "url": "https://example.com",
  "block_ads": true,
  "block_cookie_banners": true
}

Output formats

PDFs, JSON metadata, and LLM-ready content.

PDF export

Landscape A4 with CSS backgrounds — ideal for invoices and reports.

format=pdfpdf_print_backgroundpdf_landscapepdf_paper_format
curl "https://api.snapshotflow.com/screenshot?url=https://example.com&format=pdf&pdf_print_background=true&pdf_landscape=true&pdf_paper_format=a4" \
  -H "X-Api-Key: $API_KEY" -o report.pdf
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&format=pdf&pdf_print_background=true&pdf_landscape=true&pdf_paper_format=a4
const pdf = await client.take({
  url: "https://example.com",
  format: "pdf",
  pdfPrintBackground: true,
  pdfLandscape: true,
  pdfPaperFormat: "a4",
});
await pdf.save("report.pdf");
ScreenshotResult pdf = client.take(
    TakeOptions.url("https://example.com")
        .format("pdf")
        .pdfPrintBackground(true)
        .pdfLandscape(true)
        .pdfPaperFormat("a4"));
pdf.save(Path.of("report.pdf"));
# PDF export is API/SDK-only — the MCP screenshot tool
# returns png | jpeg | webp. For a "read this page" workflow,
# extract_text is usually what the agent wants instead:
extract_text {
  "url": "https://example.com",
  "format": "markdown"
}

LLM context (image + markdown)

Return a base64 image plus the page as markdown and metadata in one call.

response_type=base64extract_contentcontent_formatmetadata
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&response_type=base64&extract_content=true&content_format=markdown&metadata=true" \
  -H "X-Api-Key: $API_KEY"
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&response_type=base64&extract_content=true&content_format=markdown&metadata=true
const res = await client.takeJson({
  url: "https://stripe.com",
  responseType: "base64",
  extractContent: true,
  contentFormat: "markdown",
  metadata: true,
});
console.log(res.metadata?.title);
console.log(res.content?.slice(0, 200));   // markdown
// res.image -> "data:image/png;base64,..."
ScreenshotJson res = client.takeJson(
    TakeOptions.url("https://stripe.com")
        .responseType("base64")
        .extractContent(true)
        .contentFormat("markdown")
        .metadata(true));

System.out.println(res.metadata().title());
System.out.println(res.content());   // markdown
// res.image() -> "data:image/png;base64,..."
# Ask your agent:
"Screenshot stripe.com and summarize the page content."

# One tool call returns the image, markdown text, and metadata:
screenshot {
  "url": "https://stripe.com",
  "extract_content": true,
  "metadata": true
}

# Text-only (5-10x faster and far cheaper in tokens):
extract_text { "url": "https://stripe.com", "format": "markdown" }

Hosted download URL

Skip the bytes — get back a signed storage URL you can drop into an <img>.

response_type=url
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&response_type=url" \
  -H "X-Api-Key: $API_KEY"
# -> https://storage.googleapis.com/...signed...
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&response_type=url
const url = await client.takeUrl({ url: "https://stripe.com" });
// 1-hour signed URL, ready for an <img src>
console.log(url);
String url = client.takeUrl(
    TakeOptions.url("https://stripe.com"));
// 1-hour signed URL, ready for an <img src>
System.out.println(url);
# Hosted download URLs (response_type=url) are API/SDK-only.
# MCP delivers the capture directly into the agent's context:
screenshot { "url": "https://stripe.com" }

Force a fresh capture

Bypass the cache when you need the very latest render (cache hits are free).

cache=false
curl "https://api.snapshotflow.com/screenshot?url=https://example.com&cache=false" \
  -H "X-Api-Key: $API_KEY" -o fresh.png
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&cache=false
const shot = await client.take({
  url: "https://example.com",
  cache: false,
});
await shot.save("fresh.png");
ScreenshotResult shot = client.take(
    TakeOptions.url("https://example.com")
        .cache(false));
shot.save(Path.of("fresh.png"));
# Cache control is API/SDK-only over MCP — captures go
# through the same engine and cache as the API:
screenshot { "url": "https://example.com" }

Batch, diff & async

Multi-URL jobs, visual regression, and webhooks.

Batch capture (up to 10 URLs)

Render many pages in one parallel request; each result is independent.

POST /batchurlsresponse_type
curl -X POST "https://api.snapshotflow.com/batch" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $API_KEY" \
  -d '{
    "urls": ["https://stripe.com", "https://vercel.com"],
    "full_page": true,
    "response_type": "base64"
  }'
POST https://api.snapshotflow.com/batch?api_key=e2e_your_key_here

{
  "urls": ["https://stripe.com", "https://vercel.com"],
  "full_page": true,
  "response_type": "base64"
}
const batch = await client.batch({
  urls: ["https://stripe.com", "https://vercel.com"],
  fullPage: true,
  responseType: "base64",
});
console.log("total:", batch.total);
for (const r of batch.results) {
  if (r.error) console.warn(r.url, r.error);
  else console.log(r.url, r.cached);
}
BatchResult batch = client.batch(
    BatchOptions.urls("https://stripe.com", "https://vercel.com")
        .fullPage(true)
        .responseType("base64"));

for (BatchItem r : batch.results()) {
    if (r.error() != null) System.out.println("Failed: " + r.url() + " - " + r.error());
    else System.out.println("OK: " + r.url() + " cached=" + r.cached());
}
# Ask your agent:
"Screenshot our pricing, docs, and blog pages and flag
 anything that looks broken."

# Tool call the agent makes (1-10 URLs, one request):
batch_screenshot {
  "urls": ["https://stripe.com", "https://vercel.com"],
  "format": "png",
  "width": 1280
}

Visual diff (before / after)

Pixel-compare two URLs. Charges 2 quota units — one per render.

GET /diffbeforeafterthreshold
curl -G "https://api.snapshotflow.com/diff" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "before=https://myapp.com/v1" \
  --data-urlencode "after=https://myapp.com/v2" \
  --data-urlencode "threshold=0.1" \
  --data-urlencode "response_type=json"
# -> { "changed_pixels": 14320, "diff_percent": 1.40, "has_changes": true }
https://api.snapshotflow.com/diff?api_key=e2e_your_key_here&before=https://apple.com&after=https://apple.com/shop&threshold=0.1&response_type=json
const diff = await client.diff({
  before: "https://myapp.com/v1",
  after: "https://myapp.com/v2",
  threshold: 0.1,
  responseType: "json",
});
console.log(diff.diff_percent, diff.changed_pixels, diff.has_changes);
DiffResult diff = client.diff(
    DiffOptions.between("https://myapp.com/v1", "https://myapp.com/v2")
        .threshold(0.1)
        .responseType("json"));

System.out.println("Changed " + diff.diffPercent() + "% of pixels");
if (diff.hasChanges()) {
    // fail the CI build, alert, or save the diff image
}
# Ask your agent:
"Compare https://myapp.com/v1 with https://myapp.com/v2
 and list the visual differences."

# Tool call the agent makes:
visual_diff {
  "before": "https://myapp.com/v1",
  "after": "https://myapp.com/v2",
  "threshold": 0.1
}
# Returns change stats + a highlighted diff image the
# agent can describe back to you.

Correlate & report async jobs

Tag a job with your own ID (echoed back as X-SnapshotFlow-External-Id) and opt in to failure webhooks.

asyncwebhook_urlexternal_identifierwebhook_errors
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&async=true&webhook_url=https://yourapp.com/hooks&external_identifier=order-42&webhook_errors=true" \
  -H "X-Api-Key: $API_KEY"
# The webhook POST carries header: X-SnapshotFlow-External-Id: order-42
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&async=true&webhook_url=https://yourapp.com/hooks&external_identifier=order-42&webhook_errors=true
const job = await client.takeAsync({
  url: "https://stripe.com",
  webhookUrl: "https://yourapp.com/hooks",
  externalIdentifier: "order-42",   // echoed back for correlation
  webhookErrors: true,              // also POST when the job fails
});
console.log(job.jobId, job.status);
Job job = client.takeAsync(
    TakeOptions.url("https://stripe.com")
        .webhookUrl("https://yourapp.com/hooks")
        .externalIdentifier("order-42")   // echoed back for correlation
        .webhookErrors(true));            // also POST when the job fails

System.out.println(job.id() + " " + job.status());
# Webhooks and external identifiers are for your backend
# (API/SDK). Agents follow async jobs by polling instead:
check_job { "job_id": "550e8400-..." }

Async capture with a webhook

Queue a heavier job, get a job_id back immediately, and either poll the job or receive a webhook when it finishes.

asyncwebhook_urlGET /jobs/:id
# 1. Enqueue — returns 202 + { "job_id": "...", "status": "pending" }
curl "https://api.snapshotflow.com/screenshot?url=https://stripe.com&async=true&webhook_url=https://yourapp.com/hooks/snapshot" \
  -H "X-Api-Key: $API_KEY"

# 2. (Optional) Poll the job instead of waiting for the webhook
curl "https://api.snapshotflow.com/jobs/550e8400-..." -H "X-Api-Key: $API_KEY"
# -> { "status": "done", "result": { "storagePath": "https://..." } }
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&async=true&webhook_url=https://yourapp.com/hooks/snapshot

https://api.snapshotflow.com/jobs/550e8400-...?api_key=e2e_your_key_here
// Start the job
const { jobId, status } = await client.takeAsync({
  url: "https://stripe.com",
  webhookUrl: "https://yourapp.com/hooks/snapshot",
});
console.log(jobId, status);   // "pending"

// Either poll once...
const job = await client.getJob(jobId);

// ...or wait until it finishes (done | failed | timeout)
const done = await client.waitForJob(jobId);
console.log(done.result?.storagePath);
// Start the job
Job job = client.takeAsync(
    TakeOptions.url("https://stripe.com")
        .webhookUrl("https://yourapp.com/hooks/snapshot"));
System.out.println(job.id() + " " + job.status());   // PENDING

// Either poll once...
Job status = client.getJob(job.id());

// ...or block until it finishes (done | failed | timeout)
Job done = client.waitForJob(job.id());
System.out.println(done.status());   // PENDING -> PROCESSING -> DONE
# Ask your agent:
"Check the status of my SnapshotFlow screenshot job and
 return the result when it is complete."

# Tool call the agent makes (repeats until done):
check_job { "job_id": "550e8400-..." }
# -> { "status": "done", "result": { ... } }

Verify an incoming webhook

Validate the X-SnapshotFlow-Signature header against the raw request body — never re-serialize first.

verifyWebhookX-SnapshotFlow-Signature
# The webhook is an HTTP POST your server receives.
# Verification is HMAC-SHA256 over the raw body, keyed by your
# webhook secret. Use the SDK helper (right tab) — or replicate
# the t=...,sha256=... scheme in your language of choice.
#
# Header: X-SnapshotFlow-Signature: t=1716309930,sha256=9f86d0...
POST https://yourapp.com/hooks/snapshot
X-SnapshotFlow-Signature: t=1716309930,sha256=9f86d0...

{
  "job_id": "550e8400-...",
  "status": "done",
  "external_identifier": "order-42"
}
import { SnapshotFlow } from "snapshotflow";

// In your webhook route — pass the RAW body bytes.
const ok = SnapshotFlow.verifyWebhook({
  rawBody,                                       // raw request body
  signatureHeader: req.header("x-snapshotflow-signature"),
  secret: process.env.SNAPSHOTFLOW_WEBHOOK_SECRET,
  // toleranceSec: 300,  // freshness window; 0 disables
});
if (!ok) return res.status(400).end();
import com.snapshotflow.SnapshotFlow;

// In your webhook route — pass the RAW body bytes.
boolean ok = SnapshotFlow.verifyWebhook(
    rawBody,                                 // raw bytes - do NOT re-serialize
    request.getHeader("X-SnapshotFlow-Signature"),
    System.getenv("SNAPSHOTFLOW_WEBHOOK_SECRET"));

if (!ok) {
    // reject the request - 400
}
# Webhook verification happens on your server, not in the
# agent — use the SDK helpers (Node/Java tabs). Agents that
# need job results poll instead:
check_job { "job_id": "550e8400-..." }

Error handling

Handle quota, rate limits, and bad URLs.

Errors return a machine-readable error code; the SDK maps each one to a typed class so you can branch on it.

# Non-2xx responses carry a JSON body:
# { "error": "QUOTA_EXCEEDED", "message": "Used 300 of 300.", "quota": {...} }
curl -s -o out.png -w "%{http_code}\n" \
  "https://api.snapshotflow.com/screenshot?url=https://example.com" \
  -H "X-Api-Key: $API_KEY"
# 200 ok · 402 QUOTA_EXCEEDED · 429 RATE_LIMITED (see Retry-After) · 400 INVALID_URL
https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com

# Non-2xx responses carry JSON:
# 402 QUOTA_EXCEEDED · 429 RATE_LIMITED · 400 INVALID_URL
import { QuotaExceededError, RateLimitError, InvalidUrlError } from "snapshotflow";

try {
  const shot = await client.take({ url: "https://example.com" });
  await shot.save("out.png");
} catch (err) {
  if (err instanceof RateLimitError) {
    // back off and retry later
  } else if (err instanceof QuotaExceededError) {
    console.log(err.details?.quota);   // { used, limit, remaining }
  } else if (err instanceof InvalidUrlError) {
    console.log("bad url");
  } else {
    throw err;
  }
}
// Typed subclasses of SnapshotFlowException map to API error codes.
try {
    ScreenshotResult shot = client.take(
        TakeOptions.url("https://example.com"));
    shot.save(Path.of("out.png"));
} catch (RateLimitException e) {
    // back off and retry later
} catch (QuotaExceededException e) {
    System.out.println(e.getDetails());   // e.g. remaining quota
} catch (InvalidUrlException e) {
    System.out.println("bad url");
} catch (SnapshotFlowException e) {
    // catch-all for any other API error
    throw e;
}
# MCP tool errors come back as structured messages the agent
# reads and acts on by itself — no code to write:
#
#   401 missing_token   -> the client re-runs the OAuth sign-in
#   RATE_LIMITED        -> the agent backs off and retries
#   QUOTA_EXCEEDED      -> the agent reports remaining quota
#   INVALID_URL         -> the agent fixes the URL and retries
#
# Ask your agent:
"Screenshot example.com — if the capture fails, tell me why."

Full reference

Every /screenshot parameter in one request.

A single payload that sets every supported option. It's a reference, not a recommended combo — some keys are mutually exclusive in practice (e.g. selector vs full_page vs clip_*). Keep the ones you need and drop the rest.

cURL — every query parameter

curl -G "https://api.snapshotflow.com/screenshot" \
  -H "X-Api-Key: $API_KEY" \
  --data-urlencode "url=https://stripe.com" \
  --data-urlencode "width=1440" \
  --data-urlencode "height=900" \
  --data-urlencode "device_scale_factor=2" \
  --data-urlencode "viewport_mobile=false" \
  --data-urlencode "format=jpeg" \
  --data-urlencode "quality=90" \
  --data-urlencode "full_page=true" \
  --data-urlencode "omit_background=false" \
  --data-urlencode "selector=main" \
  --data-urlencode "clip_x=0" \
  --data-urlencode "clip_y=0" \
  --data-urlencode "clip_width=1440" \
  --data-urlencode "clip_height=900" \
  --data-urlencode "image_width=720" \
  --data-urlencode "image_height=450" \
  --data-urlencode "delay=500" \
  --data-urlencode "wait_until=networkidle0" \
  --data-urlencode "wait_for_selector=#app" \
  --data-urlencode "dark_mode=true" \
  --data-urlencode "reduced_motion=true" \
  --data-urlencode "media_type=screen" \
  --data-urlencode "timezone=Europe/Paris" \
  --data-urlencode "geolocation_latitude=48.8566" \
  --data-urlencode "geolocation_longitude=2.3522" \
  --data-urlencode "geolocation_accuracy=50" \
  --data-urlencode "user_agent=SnapshotFlowBot/1.0" \
  --data-urlencode 'headers={"Authorization":"Bearer token123"}' \
  --data-urlencode 'cookies=[{"name":"session","value":"abc","domain":"stripe.com"}]' \
  --data-urlencode "scripts=document.body.classList.add('snap')" \
  --data-urlencode "styles=.cookie-banner{display:none}" \
  --data-urlencode "hide_selectors=.intercom-launcher,.ad-slot" \
  --data-urlencode "click=#accept-cookies" \
  --data-urlencode "block_ads=true" \
  --data-urlencode "block_trackers=true" \
  --data-urlencode "block_cookie_banners=true" \
  --data-urlencode "block_requests=*.hotjar.com,*/analytics.js" \
  --data-urlencode "extract_content=true" \
  --data-urlencode "content_format=markdown" \
  --data-urlencode "metadata=true" \
  --data-urlencode "cache=true" \
  --data-urlencode "response_type=base64"

URL — every query parameter

https://api.snapshotflow.com/screenshot?api_key=e2e_your_key_here&url=https://apple.com&width=1440&height=900&device_scale_factor=2&viewport_mobile=false&format=jpeg&quality=90&full_page=true&omit_background=false&selector=main&clip_x=0&clip_y=0&clip_width=1440&clip_height=900&image_width=720&image_height=450&delay=500&wait_until=networkidle0&wait_for_selector=main&dark_mode=true&reduced_motion=true&media_type=screen&timezone=Europe/Paris&geolocation_latitude=48.8566&geolocation_longitude=2.3522&geolocation_accuracy=50&user_agent=SnapshotFlowBot/1.0&headers=%7B%22Authorization%22%3A%22Bearer%20token123%22%7D&cookies=%5B%7B%22name%22%3A%22session%22%2C%22value%22%3A%22abc%22%2C%22domain%22%3A%22apple.com%22%7D%5D&scripts=document.body.classList.add%28%27snap%27%29&styles=.cookie-banner%7Bdisplay%3Anone%7D&hide_selectors=.intercom-launcher,.ad-slot&click=button&block_ads=true&block_trackers=true&block_cookie_banners=true&block_requests=*.hotjar.com,*/analytics.js&extract_content=true&content_format=markdown&metadata=true&cache=true&response_type=base64&pdf_print_background=true&pdf_landscape=true&pdf_paper_format=a4&async=true&webhook_url=https://yourapp.com/hooks&external_identifier=order-42&webhook_errors=true

Node SDK — every option (camelCase)

const result = await client.takeJson({
  // Source
  url: "https://stripe.com",                 // or html: "<h1>Hi</h1>"
  // Viewport
  width: 1440,
  height: 900,
  deviceScaleFactor: 2,
  viewportMobile: false,
  // Capture
  format: "jpeg",                            // png | jpeg | webp | pdf
  quality: 90,
  fullPage: true,
  omitBackground: false,
  selector: "main",
  clipX: 0, clipY: 0, clipWidth: 1440, clipHeight: 900,
  imageWidth: 720, imageHeight: 450,
  // Timing
  delay: 500,
  waitUntil: "networkidle0",
  waitForSelector: "#app",
  // Emulation
  darkMode: true,
  reducedMotion: true,
  mediaType: "screen",                       // screen | print
  timezone: "Europe/Paris",
  geolocationLatitude: 48.8566,
  geolocationLongitude: 2.3522,
  geolocationAccuracy: 50,
  userAgent: "SnapshotFlowBot/1.0",
  // Injection & interaction
  headers: { Authorization: "Bearer token123" },
  cookies: [{ name: "session", value: "abc", domain: "stripe.com" }],
  scripts: "document.body.classList.add('snap')",
  styles: ".cookie-banner{display:none}",
  hideSelectors: [".intercom-launcher", ".ad-slot"],
  click: "#accept-cookies",
  // Blocking
  blockAds: true,
  blockTrackers: true,
  blockCookieBanners: true,
  blockRequests: ["*.hotjar.com", "*/analytics.js"],
  // Content & metadata
  extractContent: true,
  contentFormat: "markdown",                 // html | markdown | text
  metadata: true,
  // Cache & response
  cache: true,
  responseType: "base64",                    // image | json | base64 | url
});

console.log(result.metadata?.title, result.content?.length);

Java SDK — every option on the TakeOptions builder

ScreenshotJson result = client.takeJson(
    TakeOptions.url("https://stripe.com")          // or TakeOptions.html("<h1>Hi</h1>")
        // Viewport
        .width(1440)
        .height(900)
        .deviceScaleFactor(2)
        .viewportMobile(false)
        // Capture
        .format("jpeg")                            // png | jpeg | webp | pdf
        .quality(90)
        .fullPage(true)
        .omitBackground(false)
        .selector("main")
        .clip(0, 0, 1440, 900)
        .imageWidth(720)
        .imageHeight(450)
        // Timing
        .delay(500)
        .waitUntil("networkidle0")
        .waitForSelector("#app")
        // Emulation
        .darkMode(true)
        .reducedMotion(true)
        .mediaType("screen")                       // screen | print
        .timezone("Europe/Paris")
        .geolocation(48.8566, 2.3522, 50)
        .userAgent("SnapshotFlowBot/1.0")
        // Injection & interaction
        .headers(Map.of("Authorization", "Bearer token123"))
        .cookie("session", "abc", "stripe.com")
        .scripts("document.body.classList.add('snap')")
        .styles(".cookie-banner{display:none}")
        .hideSelectors(".intercom-launcher", ".ad-slot")
        .click("#accept-cookies")
        // Blocking
        .blockAds(true)
        .blockTrackers(true)
        .blockCookieBanners(true)
        .blockRequests("*.hotjar.com", "*/analytics.js")
        // Content & metadata
        .extractContent(true)
        .contentFormat("markdown")                 // html | markdown | text
        .metadata(true)
        // Cache & response
        .cache(true)
        .responseType("base64"));                  // image | json | base64 | url

System.out.println(result.metadata().title());

Agents — all five MCP tools and every parameter they accept

# Full input schemas: https://snapshotflow.com/.well-known/mcp/server-card.json

screenshot {
  "url": "https://stripe.com",        # or "html": "<base64>"
  "width": 1280,                      # 100-3840, default 1280
  "height": 800,                      # 100-2160, default 800
  "format": "png",                    # png | jpeg | webp
  "full_page": false,
  "dark_mode": false,
  "wait_for_selector": "#app",
  "delay": 0,                         # 0-10000 ms
  "block_ads": false,
  "block_cookie_banners": false,
  "extract_content": false,           # page text as markdown
  "metadata": false                   # title, OG tags, favicon
}

batch_screenshot {
  "urls": ["https://a.com", "https://b.com"],   # 1-10 URLs
  "format": "png", "width": 1280, "height": 800
}

visual_diff {
  "before": "https://myapp.com/v1",
  "after": "https://myapp.com/v2",
  "threshold": 0.1, "width": 1280, "height": 800
}

extract_text {
  "url": "https://stripe.com",
  "format": "markdown",               # markdown | text | html
  "max_chars": 10000
}

check_job { "job_id": "550e8400-..." }

PDF-only keys (pdf_print_background, pdf_landscape, pdf_paper_format) apply when format=pdf; async keys (async, webhook_url, external_identifier, webhook_errors) are shown above as production recipes and included in the URL reference.

MCP agents

Give Claude, Cursor, and other AI agents these tools.

Every recipe on this page is also available as a native tool over the Model Context Protocol — no code required. Connect once and your agent can capture, batch, diff, and read pages by itself.

Connect the MCP server

One remote endpoint, protected by OAuth 2.0 — your MCP client opens a browser to sign in on first connect. No API key header to manage.

ClaudeCursorCodexStreamable HTTP
{
  "mcpServers": {
    "snapshotflow": {
      "url": "https://api.snapshotflow.com/mcp"
    }
  }
}

Full tool schemas are published in the MCP server card · per-client setup guides on the MCP page.

Available tools

The server exposes five tools that map to the endpoints above.

  • screenshot — capture a URL or base64 HTML as PNG/JPEG/WebP, with full-page, dark mode, blocking, and content extraction.
  • batch_screenshot — capture 1–10 URLs in one parallel request.
  • visual_diff — pixel-compare two URLs, returns a highlighted diff image plus stats.
  • extract_text — read a page as markdown or text without rendering an image — cheaper than a screenshot.
  • check_job — follow up on an async capture job.

Prompts to try

Once connected, ask your agent in plain language — it picks the right tool and parameters.

Capture a page: “Take a full-page screenshot of https://example.com and describe the main layout issues.”

Visual regression: “Compare https://myapp.com/v1 with https://myapp.com/v2 and list the visual differences.”

Audit many pages: “Screenshot our pricing, docs, and blog pages and flag anything that looks broken.”

Read a page cheaply: “Extract the text of https://example.com/changelog as markdown and summarize what changed.”

Try these with 300 free screenshots.

Create an account, issue an API key, and run any example above against the hosted API — no credit card required.