DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Spring Boot Screenshot API: Capture Web Pages from Your Java App in Minutes

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Test it:

curl -X POST "http://localhost:8080/api/capture/screenshot?url=https://pagebolt.dev" \
  -H "Accept: image/png" \
  -o screenshot.png
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Next Steps

  1. Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
  2. Add to your Spring Boot app: Copy the ScreenshotService class and configure your RestTemplate
  3. Create a controller endpoint and start capturing screenshots
  4. 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)