Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Pricing Docs Blog Log In Sign Up

Java Screenshot API — Capture Websites with One HTTP Call

Selenium and Playwright work well for end-to-end testing, but using them purely for screenshots turns your Java service into a browser host. Each headless Chrome instance adds 200-400 MB to your JVM process. Docker images balloon past two gigabytes. Thread safety becomes a concern the moment you try to share browser instances across request threads in Spring Boot.

The ScreenshotRun Java screenshot API replaces that stack with a single HTTP call using the built-in java.net.http.HttpClient from Java 11+. Pass a URL, receive a PNG, JPEG, WebP, AVIF, or PDF. No browser engine to install, no WebDriver dependencies, no native process management.

Your first Java screenshot API call in 30 seconds

Create an account at the dashboard to get an API key (free plan, 200 captures per month, no credit card). Then run this:

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Screenshot {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newHttpClient();

        var url = URLEncoder.encode("https://github.com", StandardCharsets.UTF_8);
        var endpoint = "https://api.screenshotrun.com/v1/screenshots/capture"
                + "?url=" + url + "&response_type=image";

        var request = HttpRequest.newBuilder()
                .uri(URI.create(endpoint))
                .header("Authorization", "Bearer YOUR_API_KEY")
                .GET()
                .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
        Files.write(Path.of("github.png"), response.body());
        System.out.println("Saved: github.png");
    }
}

That covers the whole process. No browser binary download, no WebDriverManager, no Selenium dependencies in your pom.xml. The API launches a real Chrome browser on our infrastructure, waits for the page to finish rendering, and sends back the result as raw bytes.

Full example with error handling

A production-ready version needs proper parameter building, status code checks, and environment-based configuration:

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;
import java.util.stream.Collectors;

public class ScreenshotCapture {

    private static final String API_BASE = "https://api.screenshotrun.com/v1/screenshots/capture";
    private final HttpClient client;
    private final String apiKey;

    public ScreenshotCapture(String apiKey) {
        this.apiKey = apiKey;
        this.client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
    }

    public byte[] capture(String targetUrl, Map<String, String> options) throws Exception {
        var params = new java.util.LinkedHashMap<String, String>();
        params.put("url", targetUrl);
        params.put("format", options.getOrDefault("format", "png"));
        params.put("width", options.getOrDefault("width", "1280"));
        params.put("height", options.getOrDefault("height", "720"));
        params.put("full_page", options.getOrDefault("full_page", "false"));
        params.put("response_type", "image");
        params.putAll(options);

        var query = params.entrySet().stream()
                .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
                        + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
                .collect(Collectors.joining("&"));

        var request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + "?" + query))
                .header("Authorization", "Bearer " + apiKey)
                .timeout(Duration.ofSeconds(60))
                .GET()
                .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() != 200) {
            throw new RuntimeException("Screenshot API returned HTTP " + response.statusCode()
                    + ": " + new String(response.body(), StandardCharsets.UTF_8));
        }

        return response.body();
    }

    public static void main(String[] args) throws Exception {
        var apiKey = System.getenv("SCREENSHOTRUN_KEY");
        var capture = new ScreenshotCapture(apiKey);

        var screenshot = capture.capture("https://github.com", Map.of(
                "format", "webp",
                "width", "1920",
                "full_page", "true"
        ));

        Files.write(Path.of("github-full.webp"), screenshot);
        System.out.println("Captured full-page WebP screenshot");
    }
}

Store your API key in an environment variable (SCREENSHOTRUN_KEY) rather than hardcoding it. This keeps credentials out of version control and lets you rotate keys without recompiling.

Using OkHttp instead of HttpClient

If your project already depends on OkHttp for other API calls, there is no reason to mix HTTP clients. The integration looks similar but uses OkHttp's fluent builder:

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.HttpUrl;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

public class OkHttpScreenshot {

    private final OkHttpClient client = new OkHttpClient.Builder()
            .callTimeout(60, TimeUnit.SECONDS)
            .build();

    public byte[] capture(String targetUrl, String format, int width) throws Exception {
        var url = HttpUrl.parse("https://api.screenshotrun.com/v1/screenshots/capture")
                .newBuilder()
                .addQueryParameter("url", targetUrl)
                .addQueryParameter("format", format)
                .addQueryParameter("width", String.valueOf(width))
                .addQueryParameter("response_type", "image")
                .build();

        var request = new Request.Builder()
                .url(url)
                .header("Authorization", "Bearer " + System.getenv("SCREENSHOTRUN_KEY"))
                .get()
                .build();

        try (var response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new RuntimeException("API error " + response.code()
                        + ": " + response.body().string());
            }
            return response.body().bytes();
        }
    }

    public static void main(String[] args) throws Exception {
        var screenshot = new OkHttpScreenshot();
        var image = screenshot.capture("https://stripe.com", "jpeg", 1440);
        Files.write(Path.of("stripe.jpeg"), image);
    }
}

OkHttp handles URL encoding internally through HttpUrl.Builder, so you don't need the manual URLEncoder calls. The try-with-resources block ensures the response body stream gets closed even if processing fails midway.

Available parameters

Every parameter goes into the query string of the GET request. The table below covers what you will reach for most often:

ParameterTypeDefaultDescription
urlstringThe webpage to capture
formatstringpngOutput: png, jpeg, webp, avif, pdf
widthinteger1280Viewport width in pixels
heightinteger720Viewport height in pixels
full_pagebooleanfalseCapture the entire scrollable page
devicestringdesktopDevice type: desktop, mobile, tablet
retinabooleanfalseRender at 2x pixel density
dark_modebooleanfalseForce dark color scheme
selectorstringCSS selector to capture a specific element
delayinteger0Wait N milliseconds before capturing
block_cookiesbooleanfalseRemove cookie consent banners
block_adsbooleanfalseRemove advertisements
block_chatsbooleanfalseRemove live chat widgets
cssstringInject custom CSS before capture
jsstringExecute JavaScript before capture

The full parameter reference lives in the API documentation. The list above covers about 90% of what production Java integrations actually use.

Dark mode, element capture, and CSS injection

Three capabilities show up in almost every production integration, and they all work through query parameters.

Dark mode. Add dark_mode=true to the query string, and the API sets prefers-color-scheme: dark before loading the page. Any site that respects the media query renders in its dark theme:

var params = Map.of(
    "url", "https://github.com",
    "dark_mode", "true",
    "response_type", "image"
);
var screenshot = capture.capture("https://github.com", params);

Element-level capture. Target a specific component with a CSS selector instead of capturing the full viewport. The API clips the output to that element's bounding box:

var params = Map.of(
    "url", "https://github.com/anthropics",
    "selector", ".js-repo-list",
    "response_type", "image"
);
var screenshot = capture.capture("https://github.com/anthropics", params);

CSS injection. Hide a banner or change fonts before the capture happens. Pass raw CSS through the css parameter:

var params = Map.of(
    "url", "https://example.com",
    "css", ".promo-banner { display: none !important; } body { font-family: Inter, sans-serif; }",
    "response_type", "image"
);
var screenshot = capture.capture("https://example.com", params);

All three parameters combine freely in a single request. You can capture one element, in dark mode, with injected styles, all at once.

Retry logic for production

HTTP requests fail for all kinds of reasons. Target sites go down, rate limits kick in, networks drop packets somewhere between your server and ours. A production Java service needs retry logic with backoff:

public byte[] captureWithRetry(String targetUrl, Map<String, String> options, int maxRetries)
        throws Exception {

    for (int attempt = 0; attempt <= maxRetries; attempt++) {
        var params = new java.util.LinkedHashMap<>(options);
        params.put("url", targetUrl);
        params.putIfAbsent("response_type", "image");

        var query = params.entrySet().stream()
                .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
                        + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
                .collect(Collectors.joining("&"));

        var request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + "?" + query))
                .header("Authorization", "Bearer " + apiKey)
                .timeout(Duration.ofSeconds(60))
                .GET()
                .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() == 200) {
            return response.body();
        }

        if (response.statusCode() == 429) {
            var retryAfter = response.headers().firstValue("retry-after")
                    .map(Integer::parseInt).orElse(5);
            Thread.sleep(retryAfter * 1000L);
            continue;
        }

        if (response.statusCode() >= 500 && attempt < maxRetries) {
            Thread.sleep(1000L * (attempt + 1));
            continue;
        }

        throw new RuntimeException("Screenshot API error " + response.statusCode()
                + ": " + new String(response.body(), StandardCharsets.UTF_8));
    }

    throw new RuntimeException("Max retries exceeded for: " + targetUrl);
}

The method retries on rate limits (429) and server errors (5xx) with progressive backoff. Client errors like invalid parameters or bad credentials fail immediately since retrying those would be pointless.

Spring Boot REST controller

The most common pattern in Java projects: your frontend needs screenshots, and you proxy requests through Spring Boot to keep the API key server-side.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

@RestController
public class ScreenshotController {

    private final HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    @Value("${screenshotrun.api-key}")
    private String apiKey;

    @GetMapping("/api/screenshot")
    public ResponseEntity<byte[]> capture(
            @RequestParam String url,
            @RequestParam(defaultValue = "png") String format,
            @RequestParam(defaultValue = "1280") int width) throws Exception {

        var endpoint = "https://api.screenshotrun.com/v1/screenshots/capture"
                + "?url=" + URLEncoder.encode(url, StandardCharsets.UTF_8)
                + "&format=" + format
                + "&width=" + width
                + "&response_type=image";

        var request = HttpRequest.newBuilder()
                .uri(URI.create(endpoint))
                .header("Authorization", "Bearer " + apiKey)
                .timeout(Duration.ofSeconds(60))
                .GET()
                .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() != 200) {
            return ResponseEntity.status(response.statusCode())
                    .body(response.body());
        }

        var mediaType = format.equals("jpeg") ? MediaType.IMAGE_JPEG : MediaType.IMAGE_PNG;
        return ResponseEntity.ok()
                .contentType(mediaType)
                .header("Cache-Control", "public, max-age=3600")
                .body(response.body());
    }
}

Add the key to your application.properties:

screenshotrun.api-key=${SCREENSHOTRUN_KEY}

The controller accepts a URL from your frontend, forwards it to ScreenshotRun with your server-side key, and streams the image back. Your frontend never sees the API key. The Cache-Control header tells browsers (and CDNs) to reuse the image for an hour, saving you from redundant captures.

API vs. self-hosted Selenium and Playwright

Selenium WebDriver and Playwright for Java are battle-tested tools for browser automation. If your workflow involves filling forms, navigating multi-step flows, or running end-to-end tests, they are the right tools. But when the job is converting URLs into images at scale, the operational overhead stacks up fast in a Java environment.

Self-hosted (Selenium/Playwright)ScreenshotRun API
SetupInstall ChromeDriver or Playwright browsers (400+ MB), configure DockerOne HTTP GET request
Dependenciesselenium-java + WebDriverManager + Chromium binaryZero (built-in HttpClient)
Memory usageJVM heap + 200-400 MB per Chrome instanceZero browser memory on your server
Thread safetyOne WebDriver per thread, pool management requiredHttpClient is thread-safe by default
Docker image2+ GB with Chrome and fonts installedStandard JRE image (under 200 MB)
Cookie bannersCustom selectors per site, ongoing maintenanceOne parameter: block_cookies=true
Font renderingInstall font packages on every serverConsistent across all captures
CostServer CPU/RAM + engineering hoursFree tier (200/month), then usage-based

If your application already runs Selenium for test automation and screenshots are a side feature, keep using it. If screenshots are the primary goal and you want to skip the browser infrastructure entirely, a Java screenshot API call means less code and fewer moving parts in production.

Start capturing screenshots from Java

Sign up, copy your API key, and paste it into any example above. The free plan gives you 200 captures per month with no credit card required.

Get Your Free API Key

Frequently asked questions

No. The API is a standard HTTP endpoint. Java 11 and newer include java.net.http.HttpClient in the standard library, so you can call it without adding anything to your pom.xml. If your project already uses OkHttp or Apache HttpClient, those work equally well.
Any version that can make HTTP requests. The examples on this page use var and HttpClient, which require Java 11+. If you are on Java 8, use OkHttp or Apache HttpClient instead. The API itself is version-agnostic because it is just an HTTP call.
A typical capture takes 2-5 seconds depending on the target page complexity. That is comparable to Selenium on a well-configured server. The difference shows at scale: the API handles concurrency and queuing, while a local Selenium setup needs WebDriver pool management and enough RAM for parallel browser instances.
Yes. The API runs a full Chromium browser, so JavaScript-heavy pages (React, Angular, Vue, SPAs) render completely before capture. You can add a delay parameter for pages that load data asynchronously, or use wait_for_selector to wait until a specific element appears.
Yes. java.net.http.HttpClient is thread-safe by design. You can share a single instance across all request-handling threads in your Spring Boot application. Unlike Selenium WebDriver, which requires one driver instance per thread, the HttpClient approach needs no pool or synchronization.