How to Generate PDFs from URLs with Java

Published April 4, 2026 · 7 min read · URLSnap Team

Need to generate PDFs from URLs or HTML in a Java application? Whether you're building invoice generation in Spring Boot, automated report exports, or a document archiving service, this tutorial shows you how to go from URL (or raw HTML) to a pixel-perfect PDF — no Headless Chrome to configure, no iText license to manage, no native binary dependencies.

We'll use the URLSnap REST API: send an HTTP request, receive a PDF back. Works out of the box with Java 11+'s built-in HttpClient and with Spring Boot's WebClient.

Why an API instead of iText or OpenPDF?

Libraries like iText require you to build your PDF layout imperatively in Java code — fine for data tables, painful for anything that looks like a real webpage. An API approach lets you render any HTML/CSS/JS exactly as a browser would, then hand you back the bytes.

Prerequisites

Java 11 or higher (for the built-in java.net.http.HttpClient). No external dependencies needed for the basic examples. Spring WebClient and OkHttp examples require their respective libraries.

Step 1: Get a Free API Key

curl -X POST https://urlsnap.dev/api/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'

# {"key":"us_abc123...","message":"Free tier: 20 requests/day"}

Step 2: Convert a URL to PDF (Java 11+ HttpClient)

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.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

public class PdfFromUrl {

    private static final String API_KEY = "us_your_key_here"; // get free at urlsnap.dev
    private static final String PDF_ENDPOINT = "https://urlsnap.dev/api/pdf";

    public static void main(String[] args) throws Exception {
        String targetUrl = "https://example.com";
        String encoded = URLEncoder.encode(targetUrl, StandardCharsets.UTF_8);

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15))
            .build();

        // format=A4, landscape=false, margin=20px (all optional)
        String query = "url=" + encoded + "&format=A4&landscape=false&margin=20px";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(PDF_ENDPOINT + "?" + query))
            .header("x-api-key", API_KEY)
            .timeout(Duration.ofSeconds(60))
            .GET()
            .build();

        HttpResponse<byte[]> response = client.send(
            request, HttpResponse.BodyHandlers.ofByteArray()
        );

        if (response.statusCode() != 200) {
            throw new RuntimeException("API error " + response.statusCode() +
                ": " + new String(response.body()));
        }

        Path output = Path.of("output.pdf");
        Files.write(output, response.body());
        System.out.println("PDF saved to " + output.toAbsolutePath() +
            " (" + response.body().length + " bytes)");
    }
}
💡 The API fully renders JavaScript before generating the PDF, so dynamic content (React, Vue, Angular apps) is captured correctly — no extra wait logic needed.

Step 3: Convert Raw HTML to PDF

Need to generate invoices or reports from a template? POST your HTML directly and receive a rendered PDF — no public URL required.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

public class PdfFromHtml {

    private static final String API_KEY = "us_your_key_here";

    public static void main(String[] args) throws Exception {
        String html = """
            <!DOCTYPE html>
            <html>
            <head>
              <style>
                body { font-family: Arial, sans-serif; padding: 40px; color: #333; }
                .header { border-bottom: 2px solid #4f46e5; padding-bottom: 20px; margin-bottom: 30px; }
                .invoice-row { display: flex; justify-content: space-between; margin: 8px 0; }
                .total { font-size: 1.4rem; font-weight: bold; color: #4f46e5; margin-top: 20px; }
              </style>
            </head>
            <body>
              <div class="header">
                <h1>Invoice #1042</h1>
                <p>Acme Corp · April 4, 2026</p>
              </div>
              <div class="invoice-row"><span>API Starter Plan (1 month)</span><span>$9.00</span></div>
              <div class="invoice-row"><span>Tax (10%)</span><span>$0.90</span></div>
              <hr>
              <div class="invoice-row total"><span>Total</span><span>$9.90</span></div>
            </body>
            </html>
            """;

        // Build JSON body manually (no extra dependencies needed)
        String jsonBody = "{\"html\":" + jsonString(html) + ",\"format\":\"A4\",\"margin\":\"30px\"}";

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15))
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://urlsnap.dev/api/pdf"))
            .header("x-api-key", API_KEY)
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(60))
            .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();

        HttpResponse<byte[]> response = client.send(
            request, HttpResponse.BodyHandlers.ofByteArray()
        );

        if (response.statusCode() != 200) {
            throw new RuntimeException("Error " + response.statusCode() +
                ": " + new String(response.body()));
        }

        Files.write(Path.of("invoice.pdf"), response.body());
        System.out.println("Invoice saved to invoice.pdf");
    }

    // Minimal JSON string escaper — use Jackson/Gson for production
    static String jsonString(String s) {
        return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"")
                       .replace("\n", "\\n").replace("\r", "\\r")
                       .replace("\t", "\\t") + "\"";
    }
}
💡 For production, use Jackson's ObjectMapper to serialize the JSON body safely: mapper.writeValueAsString(Map.of("html", html, "format", "A4"))

All PDF API Parameters

ParameterTypeDefaultDescription
urlstringrequired (GET)Target URL to convert to PDF
htmlstringrequired (POST)Raw HTML to render as PDF
formatstringA4Paper size: A4, Letter, Legal, A3
landscapeboolfalseLandscape orientation
marginstring20pxPage margin (any CSS value, e.g. 0.5in)

Reusable UrlSnapPdfClient

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;

/**
 * Thread-safe URLSnap PDF client.
 * Reuse a single instance across your application.
 */
public class UrlSnapPdfClient {

    private final String apiKey;
    private final HttpClient http;
    private static final String BASE = "https://urlsnap.dev/api/pdf";

    public UrlSnapPdfClient(String apiKey) {
        this.apiKey = apiKey;
        this.http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15))
            .build();
    }

    /** Convert a public URL to PDF bytes. */
    public byte[] fromUrl(String url, String pageFormat, boolean landscape, String margin)
            throws Exception {
        String query = "url=" + URLEncoder.encode(url, StandardCharsets.UTF_8)
            + "&format=" + pageFormat
            + "&landscape=" + landscape
            + "&margin=" + URLEncoder.encode(margin, StandardCharsets.UTF_8);

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(BASE + "?" + query))
            .header("x-api-key", apiKey)
            .timeout(Duration.ofSeconds(60))
            .GET().build();

        return send(req);
    }

    /** Convert raw HTML to PDF bytes. */
    public byte[] fromHtml(String html, String pageFormat, boolean landscape, String margin)
            throws Exception {
        String body = String.format(
            "{\"html\":%s,\"format\":\"%s\",\"landscape\":%b,\"margin\":\"%s\"}",
            jsonEscape(html), pageFormat, landscape, margin);

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(BASE))
            .header("x-api-key", apiKey)
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(60))
            .POST(HttpRequest.BodyPublishers.ofString(body)).build();

        return send(req);
    }

    private byte[] send(HttpRequest req) throws Exception {
        HttpResponse<byte[]> res = http.send(req, HttpResponse.BodyHandlers.ofByteArray());
        if (res.statusCode() == 429)
            throw new RuntimeException("Daily limit reached — upgrade at urlsnap.dev/#pricing");
        if (res.statusCode() != 200)
            throw new RuntimeException("PDF API error " + res.statusCode() +
                ": " + new String(res.body()));
        return res.body();
    }

    private static String jsonEscape(String s) {
        return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"")
                       .replace("\n", "\\n").replace("\r", "\\r")
                       .replace("\t", "\\t") + "\"";
    }
}

Spring Boot: Stream PDF to Browser

// PdfController.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/pdf")
public class PdfController {

    private final UrlSnapPdfClient pdfClient;

    public PdfController(@Value("${urlsnap.api-key}") String apiKey) {
        this.pdfClient = new UrlSnapPdfClient(apiKey);
    }

    /** GET /pdf/from-url?url=https://example.com */
    @GetMapping("/from-url")
    public ResponseEntity<byte[]> fromUrl(@RequestParam String url) throws Exception {
        byte[] pdf = pdfClient.fromUrl(url, "A4", false, "20px");

        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"page.pdf\"")
            .contentType(MediaType.APPLICATION_PDF)
            .body(pdf);
    }

    /** POST /pdf/invoice — body: {"html":"<h1>...</h1>"} */
    @PostMapping("/invoice")
    public ResponseEntity<byte[]> invoice(@RequestBody InvoiceRequest req) throws Exception {
        byte[] pdf = pdfClient.fromHtml(req.html(), "A4", false, "30px");

        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"invoice.pdf\"")
            .contentType(MediaType.APPLICATION_PDF)
            .body(pdf);
    }

    public record InvoiceRequest(String html) {}
}

// application.properties:
// urlsnap.api-key=us_your_key_here

Save to S3 / Google Cloud Storage

// After getting the PDF bytes, upload to any object store:
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

byte[] pdfBytes = pdfClient.fromUrl("https://example.com", "A4", false, "20px");

S3Client s3 = S3Client.create();
s3.putObject(
    PutObjectRequest.builder()
        .bucket("my-reports-bucket")
        .key("reports/2026-04-04-report.pdf")
        .contentType("application/pdf")
        .build(),
    RequestBody.fromBytes(pdfBytes)
);
System.out.println("Uploaded to S3");
💡 Generating PDFs asynchronously? Use HttpClient.sendAsync() to get a CompletableFuture<byte[]> and chain it with .thenAccept() for non-blocking report generation.

Check Your Daily Usage

import java.net.URI;
import java.net.http.*;

public class QuotaCheck {
    public static void main(String[] args) throws Exception {
        String apiKey = "us_your_key_here";
        HttpClient http = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://urlsnap.dev/api/me"))
            .header("x-api-key", apiKey)
            .GET().build();

        HttpResponse<String> response = http.send(
            request, HttpResponse.BodyHandlers.ofString()
        );
        System.out.println(response.body());
        // {"plan":"free","requests_today":3,"daily_limit":20,"requests_total":15}
    }
}

Ready to generate PDFs with Java?

Free tier: 20 PDFs/day. No Headless Chrome, no iText, no server-side browser process.

Get your free API key →