Calling SnapAPI from Java
Java 11's built-in HttpClient provides a clean async API for making HTTP requests without third-party dependencies. SnapAPI accepts standard Bearer token authentication and returns binary image bytes or JSON, both of which HttpClient handles well. For Spring Boot applications, RestTemplate and WebClient are the idiomatic choices. The examples below cover both the standard library HttpClient for plain Java applications and Spring's HTTP clients for Spring Boot applications.
// Java 11 HttpClient: basic screenshot capture
import java.net.URI;
import java.net.http.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class SnapApiExample {
private static final String API_KEY = System.getenv("SNAPAPI_KEY");
private static final String BASE = "https://api.snapapi.pics";
private static final HttpClient client = HttpClient.newHttpClient();
public static byte[] screenshot(String url, String format) throws Exception {
String encoded = URLEncoder.encode(url, StandardCharsets.UTF_8);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/screenshot?url=" + encoded + "&format=" + format + "&full_page=true"))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<byte[]> resp = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
if (resp.statusCode() != 200) {
throw new RuntimeException("API error: " + resp.statusCode());
}
return resp.body();
}
public static void main(String[] args) throws Exception {
byte[] png = screenshot("https://example.com", "png");
Files.write(Path.of("screenshot.png"), png);
System.out.println("Saved " + png.length + " bytes");
}
}
Async Screenshot with CompletableFuture
Java's CompletableFuture enables non-blocking concurrent screenshot capture. Use HttpClient's sendAsync method to initiate multiple screenshot requests concurrently and collect results with CompletableFuture.allOf. This is significantly more efficient than sequential captures for batch screenshot jobs: capturing ten pages concurrently takes the time of the slowest single capture rather than the sum of all captures.
import java.net.URI;
import java.net.http.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
public class BatchScreenshot {
private static final String API_KEY = System.getenv("SNAPAPI_KEY");
private static final HttpClient client = HttpClient.newHttpClient();
public static CompletableFuture<byte[]> screenshotAsync(String url) {
try {
String enc = URLEncoder.encode(url, StandardCharsets.UTF_8);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.snapapi.pics/screenshot?url=" + enc + "&format=png"))
.header("Authorization", "Bearer " + API_KEY)
.GET().build();
return client.sendAsync(req, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(resp -> {
if (resp.statusCode() != 200) throw new RuntimeException("Error: " + resp.statusCode());
return resp.body();
});
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
public static void main(String[] args) throws Exception {
List<String> urls = List.of(
"https://example.com",
"https://github.com",
"https://stackoverflow.com"
);
List<CompletableFuture<byte[]>> futures = urls.stream()
.map(BatchScreenshot::screenshotAsync)
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (int i = 0; i < futures.size(); i++) {
byte[] bytes = futures.get(i).get();
System.out.printf("URL %d: %d bytes%n", i, bytes.length);
}
}
}
Spring Boot Integration
In Spring Boot applications, inject the SnapAPI key from application properties and use RestTemplate or WebClient to make screenshot requests. Define a ScreenshotService bean that encapsulates the API calls, and inject it into controllers and service classes as needed. Use RestTemplate for synchronous use cases and WebClient for reactive Spring WebFlux applications.
// Spring Boot: ScreenshotService with RestTemplate
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Service
public class ScreenshotService {
@Value("${snapapi.key}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
public byte[] capture(String url, String format) {
URI uri = UriComponentsBuilder
.fromHttpUrl("https://api.snapapi.pics/screenshot")
.queryParam("url", url)
.queryParam("format", format)
.queryParam("full_page", "true")
.build().toUri();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(apiKey);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> resp = restTemplate.exchange(uri, HttpMethod.GET, entity, byte[].class);
if (!resp.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("Screenshot failed: " + resp.getStatusCode());
}
return resp.getBody();
}
}
// Spring MVC Controller: screenshot proxy endpoint
@RestController
@RequestMapping("/api")
public class ScreenshotController {
private final ScreenshotService screenshotService;
public ScreenshotController(ScreenshotService screenshotService) {
this.screenshotService = screenshotService;
}
@GetMapping("/screenshot")
public ResponseEntity<byte[]> screenshot(@RequestParam String url) {
byte[] bytes = screenshotService.capture(url, "png");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setCacheControl("public, max-age=3600");
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
}
Scheduled Monitoring with @Scheduled
Spring's @Scheduled annotation runs methods on a configured schedule without external job infrastructure. Combine it with ScreenshotService to build a website monitoring job that captures screenshots of monitored URLs on a regular interval, stores them in S3 using the AWS SDK for Java, and compares them against previous captures using a pixel comparison library. Define the monitored URL list in application properties or a database table, inject it into the monitoring service, and schedule the capture job to run every 30 minutes or on a custom cron expression.
@Service
public class MonitoringService {
private final ScreenshotService screenshotService;
private final MonitoredUrlRepository urlRepo;
private final S3Client s3;
public MonitoringService(ScreenshotService ss, MonitoredUrlRepository repo, S3Client s3) {
this.screenshotService = ss;
this.urlRepo = repo;
this.s3 = s3;
}
@Scheduled(fixedDelay = 1800000) // every 30 minutes
public void captureAll() {
urlRepo.findAll().forEach(u -> {
try {
byte[] bytes = screenshotService.capture(u.getUrl(), "png");
String key = "screenshots/" + u.getId() + "/" + System.currentTimeMillis() + ".png";
s3.putObject(r -> r.bucket("my-bucket").key(key),
software.amazon.awssdk.core.sync.RequestBody.fromBytes(bytes));
System.out.println("Captured: " + u.getUrl());
} catch (Exception e) {
System.err.println("Failed: " + u.getUrl() + " — " + e.getMessage());
}
});
}
}
Error Handling and Retry in Java
Java applications calling external APIs need explicit retry logic for transient failures. Use Spring Retry's @Retryable annotation on the ScreenshotService.capture method to automatically retry on RuntimeException with exponential backoff: annotate with @Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) and add @EnableRetry to your application configuration class. For HttpClient-based (non-Spring) applications, implement retry in a loop: attempt the request up to three times, catch IOException and http error status codes that indicate transient failures (429, 503), sleep with Thread.sleep using exponential delay, and re-throw the exception after the maximum number of attempts is exhausted. Log each retry attempt with the attempt number and delay to make retry behavior visible in application logs. Distinguish between retryable errors and non-retryable errors: a 400 Bad Request indicates a problem with the request parameters that will not be resolved by retrying, while a 503 Service Unavailable is a candidate for retry with backoff.