ScreenshotNeo

BlogHow-to

Screenshot API for Spring Boot: Quick Start and Examples

Build a Spring Boot screenshot endpoint with secure API-key handling, REST calls, capture options, error handling, and a hosted ScreenshotNeo alternative.

By the ScreenshotNeo team29 September 20269 min read

Screenshot API for Spring Boot: Quick Start and Examples

Direct answer: A Spring Boot application can capture website screenshots by accepting a validated URL in a server-side controller, sending a JSON request to a screenshot provider over HTTPS, and returning the provider response to your client. Keep the provider API key in an environment-backed Spring configuration value. You can integrate through a Java SDK when its current methods are documented, or call the provider’s REST endpoint directly for precise control over headers, timeouts, retries, and response handling.

This guide builds the direct REST route. It uses the request shape documented by Screenshot API: a POST to /api/v1/screenshot with a URL, viewport, image format, and fullPage flag. The provider documents API-key authentication and recommends an authorization header. Its documentation also shows a JavaScript response containing screenshotUrl; because response contracts and SDK signatures can change, treat the raw-response handling below as the compatibility baseline and map fields only after checking the provider’s current documentation.

1. Create the Spring Boot project

Generate a web project with Spring Initializr. Choose Maven or Gradle, Java 17 or later, and the Spring Web dependency. Spring’s quickstart recommends selecting an IDE and a supported JDK such as BellSoft Liberica 17 or 21. The Spring getting-started guide lists Java 17+, Gradle 7.5+, or Maven 3.5+ for the versions covered by that guide; confirm requirements against the Spring Boot release you select.

For Maven, the relevant dependency is:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Start the generated application with:

./mvnw spring-boot:run
# or, for a Gradle project
./gradlew bootRun

2. Keep credentials on the server

Never put a screenshot provider key in browser JavaScript, a mobile app, a public repository, or a URL that users can inspect. Store it in an environment variable and bind it to Spring configuration.

# application.yml
screenshot:
  provider:
    base-url: https://api.example-provider.invalid
    api-key: ${SCREENSHOT_PROVIDER_API_KEY}

The hostname above is a placeholder. Set base-url to the provider endpoint from its current documentation. In production, inject SCREENSHOT_PROVIDER_API_KEY through your deployment secret manager. Do not log the key or the complete outbound request URL.

3. Define a narrow capture request

Expose only the capture options your application needs. A small request object makes validation and rate limiting easier than accepting an arbitrary map.

package com.example.screenshots;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

public record CaptureRequest(
    @NotBlank String url,
    @Min(320) @Max(3840) Integer width,
    @Min(240) @Max(2160) Integer height,
    String imageFormat,
    Boolean fullPage
) {}

Add validation support:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Validate the URL separately. A screenshot endpoint that accepts arbitrary URLs can become an SSRF primitive, allowing callers to probe internal services. At minimum, require an http or https scheme. For a multi-tenant service, add an allowlist, block private and link-local address ranges after DNS resolution, and consider disabling redirects to untrusted hosts.

4. Call the provider with Spring’s HTTP client

The following service uses Spring’s RestClient. It forwards the provider’s response as text so it works whether the documented endpoint returns JSON containing a screenshot URL or another JSON result. Once you verify the provider’s current contract, replace String with a typed DTO or byte-array response as appropriate.

The server-side flow: validate capture options, call the provider, and return the documented response.
The server-side flow: validate capture options, call the provider, and return the documented response.
package com.example.screenshots;

import java.net.URI;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class ScreenshotProviderClient {
  private final RestClient client;
  private final String apiKey;

  public ScreenshotProviderClient(
      RestClient.Builder builder,
      @Value("${screenshot.provider.base-url}") String baseUrl,
      @Value("${screenshot.provider.api-key}") String apiKey) {
    this.client = builder.baseUrl(baseUrl).build();
    this.apiKey = apiKey;
  }

  public String capture(CaptureRequest request) {
    var body = new ProviderRequest(
        request.url(),
        new Viewport(defaultInt(request.width(), 1440),
                     defaultInt(request.height(), 900)),
        defaultFormat(request.imageFormat()),
        request.fullPage() == null || request.fullPage());

    return client.post()
        .uri(URI.create("/api/v1/screenshot"))
        .header("Authorization", "Bearer " + apiKey)
        .contentType(MediaType.APPLICATION_JSON)
        .body(body)
        .retrieve()
        .body(String.class);
  }

  private static int defaultInt(Integer value, int fallback) {
    return value == null ? fallback : value;
  }

  private static String defaultFormat(String value) {
    return value == null || value.isBlank() ? "png" : value.toLowerCase();
  }

  record ProviderRequest(String url, Viewport viewport,
                         String imageFormat, boolean fullPage) {}
  record Viewport(int width, int height) {}
}

If the provider uses a different field name, such as format instead of imageFormat, change the record to match its current schema. Do not assume that two screenshot APIs accept interchangeable JSON.

5. Add the controller and error handling

package com.example.screenshots;

import java.net.URI;
import java.util.Map;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/screenshots")
public class ScreenshotController {
  private final ScreenshotProviderClient provider;

  public ScreenshotController(ScreenshotProviderClient provider) {
    this.provider = provider;
  }

  @PostMapping
  public ResponseEntity<?> capture(@Valid @RequestBody CaptureRequest request) {
    if (!isHttpUrl(request.url())) {
      return ResponseEntity.badRequest()
          .body(Map.of("error", "url must use http or https"));
    }

    try {
      String providerResponse = provider.capture(request);
      return ResponseEntity.ok(Map.of("providerResponse", providerResponse));
    } catch (org.springframework.web.client.HttpStatusCodeException ex) {
      return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
          .body(Map.of("error", "screenshot provider rejected the request",
                       "providerStatus", ex.getStatusCode().value()));
    } catch (org.springframework.web.client.ResourceAccessException ex) {
      return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
          .body(Map.of("error", "screenshot provider could not be reached"));
    }
  }

  private boolean isHttpUrl(String value) {
    try {
      URI uri = URI.create(value);
      return ("http".equalsIgnoreCase(uri.getScheme())
          || "https".equalsIgnoreCase(uri.getScheme()))
          && uri.getHost() != null;
    } catch (IllegalArgumentException ex) {
      return false;
    }
  }
}

Call your application:

curl -X POST http://localhost:8080/api/screenshots \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com","width":1440,"height":900,"imageFormat":"png","fullPage":true}'

Return a provider URL only after validating it. If the provider returns image bytes, use body(byte[].class) and return ResponseEntity.ok().contentType(...).body(bytes). If it returns a job identifier, expose that identifier and add a status or webhook flow rather than holding an HTTP request open indefinitely.

SDK or REST: which route should you choose?

Decision Java SDK Direct REST
Setup Add and update a provider dependency. Use Spring’s built-in HTTP client.
API coverage Convenient when the SDK tracks every option. Immediate access to documented fields.
Control Provider controls HTTP defaults. You control timeouts, headers, retries, and logging.
Upgrade risk Method signatures can change with SDK versions. JSON contracts can change, but the request is visible.

Screenshot API lists a Java SDK for Spring Boot, Jakarta EE, and Android with the dependency org.screenshot-api:screenshot-api:1.0.0. Check the provider’s current SDK page and artifact repository before adding that coordinate; the listing does not establish current method signatures. A direct REST call is safer for a copy-ready tutorial when the full SDK API has not been verified.

Capture options that affect the result

  • Viewport: Set width and height deliberately. Responsive navigation and breakpoints can change the page substantially.
  • Format: PNG preserves sharp text and transparency; JPEG is smaller for photographic pages. Use the exact format names accepted by your provider.
  • Full page: Full-page mode captures content beyond the initial viewport. Lazy-loaded images may require provider-specific scrolling or wait options.
  • Authentication: If the target site requires login, use provider-supported headers or cookies. Never forward end-user session cookies without an explicit security design.
  • Dynamic content: Wait for a selector, a fixed delay, or network idle when the page renders asynchronously. A delay alone can make requests slower without guaranteeing readiness.

Reliability, performance, and cost

Set an outbound connect and read timeout that fits your endpoint’s SLA. Use bounded retries only for transient network failures and rate-limit responses; retrying a browser capture on every 4xx error increases cost and load without fixing the request. Add a correlation ID to your logs, but redact API keys, cookies, authorization headers, and sensitive target URLs.

Cache identical captures when freshness allows it. A cache key should include the normalized URL and every visual option that changes pixels, including viewport, format, full-page mode, headers, cookies, and custom scripts. For high traffic, queue captures and return a job ID instead of tying up servlet threads.

The researched provider documentation does not establish pricing, quotas, latency, or uptime figures. Confirm those values directly before setting concurrency limits or publishing an estimate. Measure your own end-to-end duration, provider wait time, response size, and failure rate in an environment representative of production.

Troubleshooting common failures

Symptom Likely cause Fix
401 or 403 from provider Missing, expired, or wrongly formatted key. Check the authorization scheme and secret injection. Do not print the key while debugging.
400 validation error Wrong field names, unsupported format, or invalid dimensions. Compare the JSON body with the current provider schema and allowed ranges.
Timeout Slow target page, blocked resource, or provider queue. Increase the client timeout within your request budget, add an explicit wait strategy, and return a 504 or asynchronous job result.
Blank or partial image JavaScript content had not rendered, lazy images were not loaded, or the target blocked automation. Use provider wait options, full-page behavior, or a different capture time. Record the provider’s error body.
Works locally but not in production Missing environment variable, egress firewall, DNS, or proxy issue. Verify runtime configuration and outbound HTTPS connectivity from the deployed instance.
Internal hosts can be captured Unrestricted user-supplied URLs create SSRF exposure. Enforce host allowlists and block private, loopback, link-local, and metadata IP ranges.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API, so your Spring Boot service can make one GET request instead of managing a browser. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

A clean-capture service can remove consent banners and overlays before rendering the final image.
A clean-capture service can remove consent banners and overlays before rendering the final image.

See the ScreenshotNeo API documentation for all options. The same call works from any Spring service:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo supports full-page and element captures, dark mode, device presets, retina scale, PDF output, custom CSS and JavaScript, click and wait actions, blocked resources, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Every feature is available on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can a browser call the provider directly?

It can, but exposing the provider key lets anyone spend your quota. Put the provider call behind your Spring Boot server.

Should screenshots be generated synchronously?

Synchronous requests are suitable for small, predictable captures. Use asynchronous jobs when pages are slow, full-page images are large, or traffic is bursty.

How do I test without paying for a provider?

Mock the provider client in a Spring test and return representative success, validation-error, timeout, and malformed-response payloads. Keep one small integration test for the real contract in a controlled environment if the provider supports it.

What should I store for debugging?

Store a request ID, sanitized target hostname, capture options, duration, HTTP status, and response size. Avoid storing authorization headers, cookies, or complete sensitive URLs.