Spring Boot Screenshot API: Capture Web Pages from Your Java App in Minutes
Spring Boot developers face a familiar problem: your app needs to capture web pages as screenshots or PDFs, but you don't want to manage Selenium, ChromeDriver, or WebDriver in production.
This is exactly the problem PageBolt solves. Instead of spinning up headless browsers or managing complex WebDriver configurations, you just send a REST request from your Spring @Service class. Your screenshot is ready in under a second.
The Problem: Why Selenium/WebDriver Doesn't Work for Spring Boot
If you've tried Selenium or WebDriver in Spring Boot, you know the friction:
- Dependency hell: ChromeDriver version mismatches with Chromium, compatibility issues with Spring versions, WebDriver conflicts
- Resource heavy: Each screenshot request either spawns a new browser process or queues on a shared pool — not great for containerized Spring apps
- Fragile in production: WebDriver timeouts, browser crashes, Selenium upgrades break your code, Docker images become massive (Chromium + Java runtime = 1GB+)
- Not serverless-friendly: AWS Lambda, Google Cloud Run, and other FaaS platforms don't support local browsers; you need special layers or workarounds
- Testing nightmare: Your integration tests now depend on browser availability, making CI/CD slow and unreliable
PageBolt removes all of this. Your Spring Boot service just makes an HTTP request. No dependencies. Works everywhere.
The Solution: REST API Instead of Local Browsers
Here's the whole idea in one example:
@Service
public class ScreenshotService {
private static final String PAGEBOLT_API_URL = "https://api.pagebolt.dev";
private static final String API_KEY = System.getenv("PAGEBOLT_API_KEY");
private final RestTemplate restTemplate;
public ScreenshotService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public byte[] captureScreenshot(String url) {
// That's it. One HTTP request.
String payload = String.format(
"{\"url\":\"%s\",\"format\":\"png\",\"width\":1280,\"height\":720}",
url
);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + API_KEY);
headers.set("Content-Type", "application/json");
HttpEntity<String> request = new HttpEntity<>(payload, headers);
ResponseEntity<byte[]> response = restTemplate.postForEntity(
PAGEBOLT_API_URL + "/screenshot",
request,
byte[].class
);
return response.getBody();
}
}
Done. No WebDriver. No browser management. Just HTTP.
Complete Spring Boot Example 1: REST Controller with Synchronous Screenshots
Let's build a complete Spring Boot app with a REST endpoint that captures screenshots and returns them as file downloads:
package com.example.pagebolt;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
@Service
public class ScreenshotService {
private static final String PAGEBOLT_API_URL = "https://api.pagebolt.dev";
private static final String API_KEY = System.getenv("PAGEBOLT_API_KEY");
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public ScreenshotService(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
public byte[] captureScreenshot(String url, String format, boolean fullPage) {
Map<String, Object> payload = new HashMap<>();
payload.put("url", url);
payload.put("format", format);
payload.put("width", 1280);
payload.put("height", 720);
payload.put("fullPage", fullPage);
try {
String jsonPayload = objectMapper.writeValueAsString(payload);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + API_KEY);
headers.set("Content-Type", "application/json");
HttpEntity<String> request = new HttpEntity<>(jsonPayload, headers);
ResponseEntity<byte[]> response = restTemplate.postForEntity(
PAGEBOLT_API_URL + "/screenshot",
request,
byte[].class
);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new RuntimeException("PageBolt error: " + response.getStatusCode());
}
} catch (Exception e) {
throw new RuntimeException("Failed to capture screenshot: " + e.getMessage(), e);
}
}
public byte[] capturePdf(String url) {
Map<String, Object> payload = new HashMap<>();
payload.put("url", url);
payload.put("format", "A4");
payload.put("margin", "1cm");
try {
String jsonPayload = objectMapper.writeValueAsString(payload);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + API_KEY);
headers.set("Content-Type", "application/json");
HttpEntity<String> request = new HttpEntity<>(jsonPayload, headers);
ResponseEntity<byte[]> response = restTemplate.postForEntity(
PAGEBOLT_API_URL + "/pdf",
request,
byte[].class
);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new RuntimeException("PageBolt error: " + response.getStatusCode());
}
} catch (Exception e) {
throw new RuntimeException("Failed to capture PDF: " + e.getMessage(), e);
}
}
}
Now the REST controller:
package com.example.pagebolt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/capture")
public class CaptureController {
@Autowired
private ScreenshotService screenshotService;
@PostMapping("/screenshot")
public ResponseEntity<byte[]> captureScreenshot(
@RequestParam String url,
@RequestParam(defaultValue = "png") String format,
@RequestParam(defaultValue = "true") boolean fullPage) {
byte[] screenshot = screenshotService.captureScreenshot(url, format, fullPage);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(format.equals("png") ? MediaType.IMAGE_PNG : MediaType.IMAGE_JPEG);
headers.setContentDispositionFormData("attachment", "screenshot." + format);
return ResponseEntity.ok()
.headers(headers)
.body(screenshot);
}
@PostMapping("/pdf")
public ResponseEntity<byte[]> capturePdf(@RequestParam String url) {
byte[] pdf = screenshotService.capturePdf(url);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "document.pdf");
return ResponseEntity.ok()
.headers(headers)
.body(pdf);
}
}
Configuration:
package com.example.pagebolt;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(30))
.build();
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
Test it:
curl -X POST "http://localhost:8080/api/capture/screenshot?url=https://pagebolt.dev" \
-H "Accept: image/png" \
-o screenshot.png
Complete Spring Boot Example 2: Async Screenshot Service with CompletableFuture
For high-traffic apps, you'll want async processing. Here's a pattern using Spring's async support:
package com.example.pagebolt;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.UUID;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class AsyncScreenshotService {
private final ScreenshotService screenshotService;
private final Map<String, ScreenshotJob> jobs = new ConcurrentHashMap<>();
public AsyncScreenshotService(ScreenshotService screenshotService) {
this.screenshotService = screenshotService;
}
public String captureScreenshotAsync(String url) {
String jobId = UUID.randomUUID().toString();
ScreenshotJob job = new ScreenshotJob();
job.setStatus("PENDING");
job.setUrl(url);
jobs.put(jobId, job);
// Start async task without blocking
processScreenshot(jobId, url);
return jobId;
}
@Async
public void processScreenshot(String jobId, String url) {
try {
ScreenshotJob job = jobs.get(jobId);
job.setStatus("IN_PROGRESS");
byte[] screenshot = screenshotService.captureScreenshot(url, "png", true);
job.setStatus("COMPLETED");
job.setData(screenshot);
} catch (Exception e) {
ScreenshotJob job = jobs.get(jobId);
job.setStatus("FAILED");
job.setError(e.getMessage());
}
}
public ScreenshotJob getJobStatus(String jobId) {
return jobs.get(jobId);
}
}
class ScreenshotJob {
private String status;
private String url;
private byte[] data;
private String error;
// Getters and setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public byte[] getData() { return data; }
public void setData(byte[] data) { this.data = data; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
}
REST controller for async:
package com.example.pagebolt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/capture-async")
public class AsyncCaptureController {
@Autowired
private AsyncScreenshotService asyncScreenshotService;
@PostMapping("/screenshot")
public ResponseEntity<Map<String, String>> captureScreenshotAsync(@RequestParam String url) {
String jobId = asyncScreenshotService.captureScreenshotAsync(url);
return ResponseEntity.accepted().body(Map.of(
"jobId", jobId,
"status", "queued",
"statusUrl", "/api/capture-async/status/" + jobId
));
}
@GetMapping("/status/{jobId}")
public ResponseEntity<?> getStatus(@PathVariable String jobId) {
ScreenshotJob job = asyncScreenshotService.getJobStatus(jobId);
if (job == null) {
return ResponseEntity.notFound().build();
}
if (job.getStatus().equals("PENDING") || job.getStatus().equals("IN_PROGRESS")) {
return ResponseEntity.ok(Map.of("status", job.getStatus()));
} else if (job.getStatus().equals("COMPLETED")) {
return ResponseEntity.ok(Map.of(
"status", "completed",
"downloadUrl", "/api/capture-async/download/" + jobId
));
} else {
return ResponseEntity.status(500).body(Map.of(
"status", "failed",
"error", job.getError()
));
}
}
@GetMapping("/download/{jobId}")
public ResponseEntity<byte[]> downloadScreenshot(@PathVariable String jobId) {
ScreenshotJob job = asyncScreenshotService.getJobStatus(jobId);
if (job == null || !job.getStatus().equals("COMPLETED")) {
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setContentDispositionFormData("attachment", "screenshot.png");
return ResponseEntity.ok()
.headers(headers)
.body(job.getData());
}
}
Enable async in your main class:
package com.example.pagebolt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class PageboltApplication {
public static void main(String[] args) {
SpringApplication.run(PageboltApplication.class, args);
}
}
Usage:
# Queue a screenshot
curl -X POST "http://localhost:8080/api/capture-async/screenshot?url=https://pagebolt.dev"
# Returns: { "jobId": "abc-123", "status": "queued" }
# Check status
curl "http://localhost:8080/api/capture-async/status/abc-123"
# Download when ready
curl "http://localhost:8080/api/capture-async/download/abc-123" -o screenshot.png
Comparison: PageBolt vs Selenium/WebDriver vs Self-Hosted
| Feature | PageBolt | Selenium | WebDriver |
|---|---|---|---|
| Setup complexity | 1 API key | Install ChromeDriver + Chromium | Complex WebDriver setup |
| Dependency management | None (just HttpClient) | Fragile version conflicts | Version hell |
| CPU/memory per request | Hosted (not your problem) | Spins up browser process | Heavy process management |
| JavaScript rendering | Full | Full | Full |
| Works in serverless | Yes (Lambda, Cloud Run) | No | No |
| Docker image size | Unchanged | Adds 500MB+ (Chromium) | Adds 600MB+ |
| Cost at scale | $29/mo for 10k requests | $0 but infrastructure | $0 but infrastructure |
| Maintenance burden | None | High (version updates) | Very high |
Winner for Spring Boot: PageBolt. Zero dependencies, works in containerized and serverless environments, no browser management.
Cost Analysis
PageBolt pricing:
- Free tier: 100 requests/month
- Paid: $29/month for 10,000 requests (~$0.003 per screenshot)
Self-hosted Selenium in Spring Boot:
- EC2/GKE instance: $50–$150/month
- Docker image storage (Chromium): $10–$30/month
- Developer time: 12+ hours to set up, test, debug
- Maintenance: 6+ hours/month for WebDriver updates
- Real cost: $100–$200/month + your time
At just 1,000 requests/month, PageBolt saves you money on infrastructure alone.
Real-World Example: Exporting Reports as PDFs
Here's a practical example — a Spring Boot app that generates reports and exports them as PDFs:
@Service
public class ReportService {
@Autowired
private ScreenshotService screenshotService;
public byte[] generateReportPdf(Long reportId) {
// Your report data is already rendered as HTML at this URL
String reportUrl = "https://yourapp.com/reports/" + reportId + "/view";
// Capture as PDF
return screenshotService.capturePdf(reportUrl);
}
}
@RestController
@RequestMapping("/api/reports")
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/{reportId}/export")
public ResponseEntity<byte[]> exportReportPdf(@PathVariable Long reportId) {
byte[] pdf = reportService.generateReportPdf(reportId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "report_" + reportId + ".pdf");
return ResponseEntity.ok()
.headers(headers)
.body(pdf);
}
}
Next Steps
- Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
-
Add to your Spring Boot app: Copy the
ScreenshotServiceclass and configure yourRestTemplate - Create a controller endpoint and start capturing screenshots
- Scale as needed: If you exceed 100 requests/month, upgrade to a paid plan ($29/month, cancel anytime)
Spring Boot developers shouldn't be managing headless browsers. With PageBolt, you get web capture in minutes, not weeks.
Try it free — 100 requests/month, no credit card. Start capturing screenshots now.
Top comments (0)