Screenshot API with Java: Complete Integration Guide
Screenshot API with Java: Complete Integration Guide
Java powers much of enterprise software — banking platforms, insurance systems, document management, ERP backends. If your Java application needs web screenshots for compliance archiving, report generation, or content monitoring, here's how to integrate cleanly using modern Java patterns.
Basic Request with HttpClient (Java 11+)
Java's built-in HttpClient handles this without external dependencies:
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;
public class ScreenshotClient {
private static final String API_BASE = "https://hermesforge.dev/api/screenshot";
private final HttpClient http;
private final String apiKey;
public ScreenshotClient(String apiKey) {
this.apiKey = apiKey;
this.http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
public byte[] capture(String url) throws Exception {
return capture(url, 1280, "png", false);
}
public byte[] capture(String url, int width, String format, boolean fullPage) throws Exception {
String query = String.format("url=%s&width=%d&format=%s&full_page=%s",
URLEncoder.encode(url, StandardCharsets.UTF_8),
width, format, fullPage);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "?" + query))
.header("X-API-Key", apiKey)
.timeout(Duration.ofSeconds(45))
.GET()
.build();
HttpResponse<byte[]> response = http.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 429) {
throw new RateLimitException("Rate limit exceeded");
}
if (response.statusCode() != 200) {
throw new ScreenshotApiException("API error " + response.statusCode());
}
return response.body();
}
public static class ScreenshotApiException extends RuntimeException {
public ScreenshotApiException(String message) { super(message); }
}
public static class RateLimitException extends ScreenshotApiException {
public RateLimitException(String message) { super(message); }
}
}
With Retry Logic and Exponential Backoff
Production-grade client with configurable retry behavior:
import java.util.concurrent.TimeUnit;
public class RetryingScreenshotClient {
private final ScreenshotClient delegate;
private final int maxRetries;
private final long baseDelayMs;
public RetryingScreenshotClient(String apiKey, int maxRetries, long baseDelayMs) {
this.delegate = new ScreenshotClient(apiKey);
this.maxRetries = maxRetries;
this.baseDelayMs = baseDelayMs;
}
public byte[] captureWithRetry(String url) throws Exception {
Exception lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
if (attempt > 0) {
long delay = baseDelayMs * (1L << (attempt - 1));
// Add jitter: ±25% of delay
long jitter = (long)(delay * 0.25 * (Math.random() * 2 - 1));
TimeUnit.MILLISECONDS.sleep(delay + jitter);
}
try {
return delegate.capture(url);
} catch (ScreenshotClient.RateLimitException | ScreenshotClient.ScreenshotApiException e) {
lastException = e;
System.err.printf("Attempt %d failed: %s%n", attempt + 1, e.getMessage());
}
}
throw new RuntimeException("Max retries exceeded", lastException);
}
}
Spring Boot Service Bean
The idiomatic Spring Boot integration — a singleton service bean with auto-configured properties:
// src/main/java/com/example/screenshot/ScreenshotService.java
package com.example.screenshot;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
@Service
public class ScreenshotService {
@Value("${screenshot.api.key}")
private String apiKey;
@Value("${screenshot.api.base-url:https://hermesforge.dev/api/screenshot}")
private String baseUrl;
@Value("${screenshot.api.timeout-seconds:45}")
private int timeoutSeconds;
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
public byte[] capture(String url, ScreenshotOptions options) throws IOException, InterruptedException {
URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl)
.queryParam("url", url)
.queryParam("width", options.getWidth())
.queryParam("format", options.getFormat())
.queryParam("full_page", options.isFullPage())
.queryParam("delay", options.getDelayMs())
.build()
.toUri();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header("X-API-Key", apiKey)
.timeout(Duration.ofSeconds(timeoutSeconds))
.GET()
.build();
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
return switch (response.statusCode()) {
case 200 -> response.body();
case 429 -> throw new ScreenshotRateLimitException("Rate limit exceeded");
case 422 -> throw new ScreenshotValidationException("Invalid parameters");
default -> throw new ScreenshotApiException("API error " + response.statusCode());
};
}
public CompletableFuture<byte[]> captureAsync(String url, ScreenshotOptions options) {
URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl)
.queryParam("url", url)
.queryParam("width", options.getWidth())
.queryParam("format", options.getFormat())
.queryParam("full_page", options.isFullPage())
.build()
.toUri();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header("X-API-Key", apiKey)
.timeout(Duration.ofSeconds(timeoutSeconds))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(response -> {
if (response.statusCode() != 200) {
throw new ScreenshotApiException("API error " + response.statusCode());
}
return response.body();
});
}
}
// ScreenshotOptions.java — value object
public class ScreenshotOptions {
private int width = 1280;
private String format = "png";
private boolean fullPage = false;
private int delayMs = 1000;
public static ScreenshotOptions defaults() { return new ScreenshotOptions(); }
public static ScreenshotOptions fullPage() {
ScreenshotOptions opts = new ScreenshotOptions();
opts.fullPage = true;
return opts;
}
// Builder-style setters
public ScreenshotOptions width(int w) { this.width = w; return this; }
public ScreenshotOptions format(String f) { this.format = f; return this; }
public ScreenshotOptions fullPage(boolean fp) { this.fullPage = fp; return this; }
public ScreenshotOptions delay(int ms) { this.delayMs = ms; return this; }
// Standard getters
public int getWidth() { return width; }
public String getFormat() { return format; }
public boolean isFullPage() { return fullPage; }
public int getDelayMs() { return delayMs; }
}
# application.properties
screenshot.api.key=${SCREENSHOT_API_KEY}
screenshot.api.timeout-seconds=45
Spring Boot REST Controller
A controller that exposes screenshot capture as an endpoint:
@RestController
@RequestMapping("/api/screenshots")
public class ScreenshotController {
private final ScreenshotService screenshotService;
public ScreenshotController(ScreenshotService screenshotService) {
this.screenshotService = screenshotService;
}
@GetMapping(produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<byte[]> capture(
@RequestParam String url,
@RequestParam(defaultValue = "1280") int width,
@RequestParam(defaultValue = "false") boolean fullPage) {
// URL validation
try {
URI parsed = new URI(url);
if (!List.of("http", "https").contains(parsed.getScheme())) {
return ResponseEntity.badRequest().build();
}
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().build();
}
try {
byte[] image = screenshotService.capture(url,
ScreenshotOptions.defaults().width(width).fullPage(fullPage));
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(image);
} catch (ScreenshotRateLimitException e) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build();
}
}
}
Async Batch Processing with CompletableFuture
Capture multiple URLs concurrently:
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class BatchScreenshotProcessor {
private final ScreenshotService service;
private final ExecutorService executor;
public BatchScreenshotProcessor(ScreenshotService service, int concurrency) {
this.service = service;
this.executor = Executors.newFixedThreadPool(concurrency);
}
public record ScreenshotResult(String url, byte[] data, Exception error) {
public boolean isSuccess() { return error == null; }
}
public List<ScreenshotResult> captureAll(List<String> urls) {
List<CompletableFuture<ScreenshotResult>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> {
try {
byte[] data = service.capture(url, ScreenshotOptions.fullPage());
return new ScreenshotResult(url, data, null);
} catch (Exception e) {
return new ScreenshotResult(url, null, e);
}
}, executor))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
public void shutdown() {
executor.shutdown();
try {
executor.awaitTermination(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Usage
BatchScreenshotProcessor processor = new BatchScreenshotProcessor(screenshotService, 3);
List<String> urls = List.of(
"https://example.com",
"https://example.org",
"https://example.net"
);
List<BatchScreenshotProcessor.ScreenshotResult> results = processor.captureAll(urls);
results.forEach(r -> {
if (r.isSuccess()) {
System.out.printf("OK %s (%d bytes)%n", r.url(), r.data().length);
} else {
System.err.printf("ERR %s: %s%n", r.url(), r.error().getMessage());
}
});
processor.shutdown();
Compliance Archiving with Spring Data JPA
For enterprise compliance use cases — storing screenshots with metadata and hash integrity:
@Entity
@Table(name = "web_archives")
public class WebArchive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String sourceUrl;
@Column(nullable = false)
private String capturedAt;
@Column(nullable = false, length = 64)
private String sha256Hash;
@Column(nullable = false)
private String storagePath;
@Column
private Long fileSizeBytes;
@Column
private String capturedBy; // system or user identifier
@Column
private String retentionCategory;
// getters, setters, constructors
}
@Service
@Transactional
public class ComplianceArchiveService {
private final ScreenshotService screenshotService;
private final WebArchiveRepository repository;
@Value("${archive.storage.path:/var/archives}")
private String storagePath;
public WebArchive archiveUrl(String url, String capturedBy, String retentionCategory) throws Exception {
byte[] image = screenshotService.capture(url,
ScreenshotOptions.fullPage().delay(3000));
// Compute integrity hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(image);
String hash = HexFormat.of().formatHex(hashBytes);
// Store to filesystem
String timestamp = Instant.now().toString().replace(":", "").replace(".", "");
String filename = String.format("%s_%s.png",
URI.create(url).getHost().replace(".", "-"), timestamp);
Path filePath = Path.of(storagePath, filename);
Files.createDirectories(filePath.getParent());
Files.write(filePath, image);
// Persist record
WebArchive archive = new WebArchive();
archive.setSourceUrl(url);
archive.setCapturedAt(Instant.now().toString());
archive.setSha256Hash(hash);
archive.setStoragePath(filePath.toString());
archive.setFileSizeBytes((long) image.length);
archive.setCapturedBy(capturedBy);
archive.setRetentionCategory(retentionCategory);
return repository.save(archive);
}
public boolean verifyIntegrity(Long archiveId) throws Exception {
WebArchive archive = repository.findById(archiveId)
.orElseThrow(() -> new EntityNotFoundException("Archive not found: " + archiveId));
byte[] storedBytes = Files.readAllBytes(Path.of(archive.getStoragePath()));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String currentHash = HexFormat.of().formatHex(digest.digest(storedBytes));
return currentHash.equals(archive.getSha256Hash());
}
}
JUnit Integration Tests
Testing the service with WireMock:
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class ScreenshotServiceTest {
@Autowired
private ScreenshotService service;
@Value("${wiremock.server.port}")
private int wireMockPort;
@BeforeEach
void setUp() {
// Override the API base URL to point to WireMock
ReflectionTestUtils.setField(service, "baseUrl",
"http://localhost:" + wireMockPort + "/api/screenshot");
}
@Test
void captureReturnsImageBytes() throws Exception {
byte[] fakeImage = new byte[]{(byte)0x89, 0x50, 0x4E, 0x47}; // PNG header
stubFor(get(urlPathEqualTo("/api/screenshot"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "image/png")
.withBody(fakeImage)));
byte[] result = service.capture("https://example.com", ScreenshotOptions.defaults());
assertArrayEquals(fakeImage, result);
}
@Test
void captureThrowsOnRateLimit() {
stubFor(get(urlPathEqualTo("/api/screenshot"))
.willReturn(aResponse().withStatus(429)));
assertThrows(ScreenshotRateLimitException.class, () ->
service.capture("https://example.com", ScreenshotOptions.defaults()));
}
}
The screenshot API is HTTP-based — any Java HTTP client works. The patterns above are production-ready for Spring Boot 3.x and Java 17+. First 100 requests free.