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:
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | — | The webpage to capture |
format | string | png | Output: png, jpeg, webp, avif, pdf |
width | integer | 1280 | Viewport width in pixels |
height | integer | 720 | Viewport height in pixels |
full_page | boolean | false | Capture the entire scrollable page |
device | string | desktop | Device type: desktop, mobile, tablet |
retina | boolean | false | Render at 2x pixel density |
dark_mode | boolean | false | Force dark color scheme |
selector | string | — | CSS selector to capture a specific element |
delay | integer | 0 | Wait N milliseconds before capturing |
block_cookies | boolean | false | Remove cookie consent banners |
block_ads | boolean | false | Remove advertisements |
block_chats | boolean | false | Remove live chat widgets |
css | string | — | Inject custom CSS before capture |
js | string | — | Execute 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 | |
|---|---|---|
| Setup | Install ChromeDriver or Playwright browsers (400+ MB), configure Docker | One HTTP GET request |
| Dependencies | selenium-java + WebDriverManager + Chromium binary | Zero (built-in HttpClient) |
| Memory usage | JVM heap + 200-400 MB per Chrome instance | Zero browser memory on your server |
| Thread safety | One WebDriver per thread, pool management required | HttpClient is thread-safe by default |
| Docker image | 2+ GB with Chrome and fonts installed | Standard JRE image (under 200 MB) |
| Cookie banners | Custom selectors per site, ongoing maintenance | One parameter: block_cookies=true |
| Font rendering | Install font packages on every server | Consistent across all captures |
| Cost | Server CPU/RAM + engineering hours | Free 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