Java · Code recipe
Take a screenshot of a website in Java
Save a full-page PNG of any website, in Java 17+ with java.net.http.HttpClient. Every program on this page runs as it stands — each one was run against a stub of the API before it was published.
By Roger Campos · Last updated: September 2026
TL;DR
To take a screenshot of a website in Java, POST its URL to https://urlpipe.dev/screenshot with sync set to true, decode the Base64 body with Base64.getDecoder() and write the bytes to a .png file. It is a full-page capture from real Chrome for 1 credit; leave sync out and the result goes to your webhook instead.
Free plan, no credit card. 1,000 credits a month.
One POST to /screenshot loads the page in real Chrome, scrolls it top to bottom so lazy images load, and captures the whole document — not just the fold. The image comes back Base64-encoded in a text/plain body, so the Java work is two lines: decode it with Base64.getDecoder() and write the bytes.
The request below also sets page_options.block_cookie_banners, because a consent dialog over the page is the most common reason a screenshot is useless. Every other knob — viewport, device scale, JPEG or WebP, one element by selector, dark mode — goes in screenshot_options.
Setup
Before you start
Java 17 or later: java.net.http is in the JDK. The async and error-handling programs read JSON, and the JDK has no parser, so those use Gson — one jar, no dependencies of its own. In a Maven or Gradle build add com.google.code.gson:gson:2.13.1; to try a file on its own, download the jar next to it.
java -version # 17 or later
curl -sSLO https://repo1.maven.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar
export URLPIPE_API_KEY="your_api_key"
The request
Save a screenshot as a PNG file
"sync": true keeps the request open until the result is ready. BodyHandlers.ofString() reads the whole body as text, decoded with the charset the response names. HttpClient does not throw on a 4xx or 5xx, so check statusCode(). The body is Base64 text; Base64.getDecoder() gives the bytes back.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Base64;
public class Screenshot {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://urlpipe.dev/screenshot"))
.header("Authorization", "Bearer " + System.getenv("URLPIPE_API_KEY"))
.header("Content-Type", "application/json")
// A sync call can take up to 60 s; HttpClient would otherwise wait forever.
.timeout(Duration.ofSeconds(90))
.POST(HttpRequest.BodyPublishers.ofString("""
{"url": "https://example.com", "page_options": {"block_cookie_banners": true}, "sync": true}
"""))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
System.err.println("URLpipe answered " + response.statusCode() + ": " + response.body());
System.exit(1);
}
String body = response.body();
byte[] png = Base64.getDecoder().decode(body.strip());
Files.write(Path.of("screenshot.png"), png);
System.out.println("Saved screenshot.png (" + png.length + " bytes)");
}
}
Run it: java Screenshot.java
Async
The async variant: a token, a webhook and a poll
Leave out sync and the answer is a token, straight away. One HttpClient is shared by every call — it holds the connection pool, so build it once — and a switch expression covers the answers GET /result/:token can give.
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Base64;
public class ScreenshotAsync {
static final String API = "https://urlpipe.dev";
static final HttpClient CLIENT = HttpClient.newHttpClient();
static HttpResponse<String> send(HttpRequest.Builder builder) throws Exception {
var request = builder
.header("Authorization", "Bearer " + System.getenv("URLPIPE_API_KEY"))
.timeout(Duration.ofSeconds(30))
.build();
return CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
}
static void fail(String message) {
System.err.println(message);
System.exit(1);
}
public static void main(String[] args) throws Exception {
// No "sync": the request is accepted at once and the work carries on without you.
var accepted = send(HttpRequest.newBuilder(URI.create(API + "/screenshot"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"url": "https://example.com",
"page_options": {"block_cookie_banners": true},
"report_to": "https://your-app.com/webhooks/urlpipe",
"labels": {"customer": "acme"}
}
""")));
if (accepted.statusCode() != 200) {
fail("URLpipe answered " + accepted.statusCode() + ": " + accepted.body());
}
String token = JsonParser.parseString(accepted.body()).getAsJsonObject().get("token").getAsString();
System.out.println("Accepted " + token);
// The result is POSTed to report_to when it is ready. Polling by token is the
// other way to collect it: no endpoint needed, and a backup for the webhook.
HttpResponse<String> response = null;
for (int attempt = 0; attempt < 60; attempt++) {
response = send(HttpRequest.newBuilder(URI.create(API + "/result/" + token)).GET());
if (response.statusCode() != 202) { // 202 means still processing
break;
}
Thread.sleep(2000);
}
switch (response.statusCode()) {
case 200 -> { }
case 202 -> fail("Still processing after two minutes; try the token again later.");
case 422 -> fail("The analysis failed: "
+ JsonParser.parseString(response.body()).getAsJsonObject().get("error").getAsString());
case 410 -> fail("The result is past the 30-day window; send the request again.");
default -> fail("URLpipe answered " + response.statusCode() + ": " + response.body());
}
String body = response.body();
byte[] png = Base64.getDecoder().decode(body.strip());
Files.write(Path.of("screenshot.png"), png);
System.out.println("Saved screenshot.png (" + png.length + " bytes)");
}
}
Run it: java -cp gson-2.13.1.jar ScreenshotAsync.java
Errors
Handle errors and retries
A checked URLpipeException makes the caller decide what a refusal means. firstValueAsLong reads Retry-After without a parse of your own, and a body that is not JSON (a 401 answers in plain text) falls back to an empty object.
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Base64;
public class ScreenshotErrors {
static class URLpipeException extends Exception {
URLpipeException(String message) {
super(message);
}
}
static final HttpClient CLIENT = HttpClient.newHttpClient();
// POST a sync request and return the result body; retry the two 429s that clear by themselves.
static String urlpipe(String path, String payload) throws Exception {
final int attempts = 5;
for (int attempt = 0; attempt < attempts; attempt++) {
var request = HttpRequest.newBuilder(URI.create("https://urlpipe.dev" + path))
.header("Authorization", "Bearer " + System.getenv("URLPIPE_API_KEY"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(90))
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
var response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status == 200) {
return response.body();
}
if (status == 401) {
throw new URLpipeException("401: the API key is missing or wrong. Check URLPIPE_API_KEY.");
}
JsonObject error;
try {
error = JsonParser.parseString(response.body()).getAsJsonObject();
} catch (RuntimeException notJson) {
error = new JsonObject();
}
String code = error.has("error") ? error.get("error").getAsString() : "";
String detail = error.has("message") ? code + ": " + error.get("message").getAsString() : code;
if (status == 429 && code.equals("rate_limited")) {
// Sending too fast: Retry-After says how long the window has left.
Thread.sleep(response.headers().firstValueAsLong("Retry-After").orElse(1) * 1000);
} else if (status == 429 && code.equals("concurrency_limit")) {
// Every parallel slot on your plan is busy with your own requests.
Thread.sleep((1L << attempt) * 1000);
} else if (status == 504) {
// Still running on our side; the token collects it from GET /result/:token.
throw new URLpipeException(
"504 processing_timeout: collect it later with token " + error.get("token").getAsString());
} else {
// 403 email_unverified, 422 (a bad parameter, or a page that would not load),
// 429 quota_exceeded: sending the same request again gets the same answer.
throw new URLpipeException(status + ": " + detail);
}
}
throw new URLpipeException("429: still refused after " + attempts + " attempts");
}
public static void main(String[] args) throws Exception {
String body;
try {
body = urlpipe("/screenshot", """
{"url": "https://example.com", "page_options": {"block_cookie_banners": true}, "sync": true}
""");
} catch (URLpipeException e) {
System.err.println("URLpipe: " + e.getMessage());
System.exit(1);
return;
}
byte[] png = Base64.getDecoder().decode(body.strip());
Files.write(Path.of("screenshot.png"), png);
System.out.println("Saved screenshot.png (" + png.length + " bytes)");
}
}
Run it: java -cp gson-2.13.1.jar ScreenshotErrors.java
Details
What to know about /screenshot
- The default viewport is 1350 × 797 and the capture follows the page down to 16,384 px; a taller page is cut there.
- Every response carries an
X-Result-Urlheader: a link to the same image that needs no API key and stays valid for 30 days. Often you can store that link instead of the file. - PNG is the default;
"format": "jpeg"or"webp"inscreenshot_optionsmakes a long page far smaller. - It captures images, not PDFs, and it does not record video.
- A screenshot of the same URL with the same options is served from the cache for 7 days by default, free. Set
max_ageto refresh sooner.
FAQ
Frequently asked questions
Why is the screenshot response Base64 and not an image?
How do I take a mobile screenshot?
Can I screenshot one element instead of the whole page?
Do I need an SDK to call URLpipe from Java?
Get a key and run it.
Free plan, no card. Paste your key into URLPIPE_API_KEY and every program on this page runs as it is.