# Spring Boot Printing: Send PDFs to Local Printers by API

> Build Spring Boot printing that sends PDFs to local printers without browser dialogs. Follow a Java API example, secure setup, and reliable job handling.

Published 2026-11-11 · https://printscaler.com/blog/spring-boot-printing-local-printers

![Cloud application sending a PDF through a local computer to an office printer producing an invoice](https://printscaler.com/blog/images/spring-boot-printing-local-printers-17.jpg)

You can print a PDF from Spring Boot to a local printer by sending it to a printing API, with an agent running on a computer that can access the printer. Your Java application makes an HTTPS request; it does not need the printer installed on the application server. This guide builds that workflow with PrintScaler, Java 17, and a Spring Boot service, then explains how to handle retries, offline computers, and print status safely.

## Why Spring Boot cannot automatically see a user's printer

A Spring Boot application runs in the environment where you deploy it. If that is a cloud VM, the printers visible to the server are not the USB and office printers available on your customer's laptop.

Java's printing APIs do not bridge that gap automatically. `PrintServiceLookup`, for example, discovers print services available to the Java runtime's environment. It does not discover arbitrary printers on another company's private network.

There is also a document-format question. Finding a print service does not guarantee that it accepts PDF documents directly. Depending on the print service and driver, printing a PDF may require rendering it through a PDF library first.

Choose the architecture according to where your application and printer run:

| Situation | Practical approach | Main consideration |
|---|---|---|
| Java application and printer share a managed workstation | Java printing APIs or an installed print service | Driver support and PDF rendering |
| A user needs to review a document before printing | Download or open the PDF for manual printing | A person completes the workflow |
| Cloud-hosted Spring Boot must print at a warehouse or office | Printing API plus a local agent | Printer mapping and delivery tracking |
| A backend must print across several customer networks | Printing API with agents at the destinations | Authorization and account isolation |

For unattended invoices, packing slips, and operational paperwork, the third approach avoids making your web server act like a local desktop.

## How Spring Boot printing works with PrintScaler

The delivery path is straightforward:

1. Spring Boot generates a PDF or obtains an existing PDF.
2. Your application selects an authorized destination printer.
3. Spring Boot submits a print job over HTTPS.
4. PrintScaler delivers the job through its agent.
5. The agent submits it to the local printing system.

The PrintScaler agent runs on Windows, macOS, Linux, and Raspberry Pi. It uses an outbound WebSocket connection, so the agent-based setup does not require opening inbound firewall ports. Your network must still permit the required outbound traffic.

The agent starts after reboots, updates itself, and reports printers and their status. If its computer is offline, jobs can queue until it reconnects, subject to a configurable expiration time.

This architecture separates three concerns: your application creates the document, PrintScaler transports the job, and the destination computer provides printer access.

## Step 1: Connect and test the destination printer

Before writing Java code, establish that the printer works locally.

1. Install the printer and its appropriate driver on the destination computer.
2. Print a test document from that computer.
3. Install the [PrintScaler agent](https://printscaler.com/download).
4. Enter the agent's pairing code in the PrintScaler dashboard.
5. Find the discovered printer and use the dashboard's test-page button.
6. Record the printer ID for your application configuration.

For your first integration, use one known printer and one simple PDF. Avoid debugging printer drivers, document generation, network access, and Java code simultaneously.

The computer running the agent must remain available when immediate printing is required. A laptop that sleeps whenever its lid closes is usually a poor choice for a continuously operating packing station.

### Keep printer selection on the server

For a single destination, store the printer ID in backend configuration. For multiple destinations, maintain an application-owned mapping between a business destination and its printer ID.

Do not accept an arbitrary `printerId` from a browser and submit it without authorization. A user permitted to print an invoice is not necessarily permitted to send it to every printer connected to your account.

## Step 2: Verify the API before adding Java code

Create an API key in the dashboard and provide it to your development shell through your normal secret-management process. The following example assumes `PRINTSCALER_API_KEY` is already set.

Submit a small, non-sensitive PDF that PrintScaler can retrieve:

```bash
curl --fail-with-body \
  --request POST \
  'https://api.printscaler.com/v1/printjobs' \
  --header "Authorization: Bearer ${PRINTSCALER_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'X-Idempotency-Key: dev-invoice-1042-initial' \
  --data '{
    "printerId": 123,
    "contentType": "pdf_uri",
    "content": "https://files.example.com/invoice-1042.pdf",
    "title": "Invoice 1042"
  }'
```

Replace the printer ID and example URL with real values. The URL must return the PDF itself, not an HTML login page or a document-preview screen.

Use a new idempotency key when you intentionally want another print. Reusing the same key is appropriate when retrying the same logical request, not when asking for an additional copy later.

Consult the [print-job API reference](https://printscaler.com/docs#printjobs) for response details and supported request fields. An accepted HTTP request is not, by itself, evidence that paper has emerged from the printer.

## Step 3: Add a Spring Boot service for PDF printing

This example targets Java 17 and Spring Boot 3.x with `spring-boot-starter-web`, which supplies Spring Web and Jackson. It uses Java's built-in HTTP client, so no separate HTTP client dependency is necessary.

The service accepts PDF bytes and submits them as `pdf_base64`. This works well when your application has already generated the document and you do not want to publish a retrievable URL.

Configure the API key and initial printer destination through environment variables:

```yaml
# application.yml
printscaler:
  api-key: ${PRINTSCALER_API_KEY}
  printer-id: ${PRINTSCALER_PRINTER_ID}
```

Create the service:

```java
package com.example.printing;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Base64;

@Service
public class PdfPrintService {
    private static final URI ENDPOINT = URI.create(
        "https://api.printscaler.com/v1/printjobs"
    );

    private final ObjectMapper json;
    private final String apiKey;
    private final long printerId;
    private final HttpClient http = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .build();

    public PdfPrintService(
        ObjectMapper json,
        @Value("${printscaler.api-key}") String apiKey,
        @Value("${printscaler.printer-id}") long printerId
    ) {
        this.json = json;
        this.apiKey = apiKey;
        this.printerId = printerId;
    }

    public String submitPdf(
        byte[] pdf,
        String title,
        String idempotencyKey
    ) throws IOException, InterruptedException {
        if (pdf == null || pdf.length == 0) {
            throw new IllegalArgumentException("PDF is empty");
        }
        if (idempotencyKey == null || idempotencyKey.isBlank()) {
            throw new IllegalArgumentException(
                "An idempotency key is required"
            );
        }

        var payload = new PdfJob(
            printerId,
            "pdf_base64",
            Base64.getEncoder().encodeToString(pdf),
            title
        );

        var request = HttpRequest.newBuilder(ENDPOINT)
            .timeout(Duration.ofSeconds(30))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .header("X-Idempotency-Key", idempotencyKey)
            .POST(HttpRequest.BodyPublishers.ofString(
                json.writeValueAsString(payload)
            ))
            .build();

        var response = http.send(
            request, HttpResponse.BodyHandlers.ofString()
        );

        if (response.statusCode() < 200
                || response.statusCode() >= 300) {
            throw new IOException(
                "Print submission returned HTTP "
                    + response.statusCode()
            );
        }

        return response.body();
    }

    private record PdfJob(
        long printerId,
        String contentType,
        String content,
        String title
    ) {}
}
```

The method returns the response body rather than assuming an undocumented response shape. In production, parse the documented response and persist the returned job identifier so you can correlate subsequent status updates.

The example deliberately makes one submission attempt. Apply a retry policy in your worker after classifying failures; do not indiscriminately retry every exception or HTTP error.

### Call the service from an authorized workflow

In an existing Spring-managed order or invoice service, inject `PdfPrintService` and call it after generating the PDF:

```java
String responseBody = pdfPrintService.submitPdf(
    invoicePdfBytes,
    "Invoice 1042",
    "invoice-1042-original-v1"
);
```

Here, `invoicePdfBytes` is the PDF produced by your document-generation code. PrintScaler delivers the PDF; it does not replace your invoice template or PDF generator.

Keep authentication, business authorization, and printer selection in your backend. Do not expose this method through an unrestricted public controller, and never send the PrintScaler API key to a browser.

## Step 4: Choose PDF bytes or a PDF URL

PrintScaler supports both `pdf_base64` and `pdf_uri`. Choose according to how your documents are stored and delivered.

| Format | Useful when | Watch for |
|---|---|---|
| `pdf_base64` | Spring Boot already holds the generated PDF bytes | Larger JSON requests and additional memory use |
| `pdf_uri` | The PDF is stored at a retrievable HTTPS URL | URL accessibility, expiry, and document exposure |

Base64 increases the encoded document size by roughly one-third. The Java example also holds the original bytes, encoded content, and serialized request in memory. For larger documents or higher concurrency, evaluate whether a retrievable URL better suits your application.

With URL-based delivery, a private browser session is not enough. A link that works only because you are logged into your document portal will not work as a direct PDF source.

If you use signed URLs, allow enough validity for the expected delivery window, including foreseeable delays. Do not use a short-lived document link without considering queued jobs and retries.

Keep API keys, signed URLs, and base64 document content out of ordinary application logs. Document titles should also avoid unnecessary personal or sensitive information.

## Step 5: Separate submission from print completion

Your application should distinguish “submitted” from “completed.” PrintScaler provides job states including `new`, `sent_to_client`, `queued`, `in_progress`, `done`, `error`, `cancelled`, and `expired`, with a full history.

A useful application record associates:

- The business document and its version.
- The authorized destination printer.
- The logical print operation and idempotency key.
- The PrintScaler job identifier.
- Submission attempts and the latest reported state.

Use [print-job webhooks](https://printscaler.com/docs#webhooks) to update this record as the job changes state. Verify their HMAC-SHA256 signatures according to the documentation, and make your event handler safe to process again because webhook delivery is retried.

Do not interpret a reported completion state as universal proof that a person received a readable document. What can be confirmed depends on the printer and printing system. For critical workflows, define how operators report missing output and request an intentional reprint.

If you need more background on retry design, see the guide to [preventing duplicate print jobs](https://printscaler.com/blog/prevent-duplicate-print-jobs).

## Make retries safe without hiding failures

A timeout is ambiguous: the server might have accepted the job even though your Java client never received the response. Retrying with a fresh identifier risks producing another copy.

Generate an idempotency key for the logical print operation and persist it before submission. Reuse that key when retrying the same operation with the same document and destination. PrintScaler's [idempotency support](https://printscaler.com/docs#idempotency) prevents a retried request from printing twice.

Treat an intentional reprint as a new operation with a new key. For example, the original invoice and an operator-requested replacement should have distinct operation identifiers.

For production processing:

- Retry transient connectivity failures with bounded exponential backoff and jitter.
- Handle rate limiting; PrintScaler's limit is 10 requests per second, with a burst of 30.
- Correct authentication and validation failures rather than repeatedly resubmitting them.
- Preserve thread interruption when handling `InterruptedException`.
- Cap attempts and surface unresolved failures to an operator.

For business-critical printing, store a print task alongside the business transaction and let a background worker submit it. Avoid making an entire order transaction depend on a synchronous network call finishing successfully.

## Troubleshoot the delivery path in order

### The API accepts the job, but nothing prints

Check the selected printer ID, computer connectivity, job history, and local operating-system queue. Then check paper, cover state, driver errors, and whether the printer itself is paused or offline.

An offline agent computer and an online computer with a jammed printer are different failure modes. Inspect the stage where progress stopped before submitting another copy.

### The PDF prints, but the layout is wrong

Confirm the PDF's page dimensions and the printer's actual paper size. A document designed for a label is not equivalent to an office-size sheet with a small label drawn in one corner.

PrintScaler supports PDF options including paper size, fit to page, rotation, trays, duplex, and copies. Read printer capabilities through the API rather than assuming every device supports the same settings. For label-specific layout problems, see the [4×6 PDF shipping-label guide](https://printscaler.com/blog/print-4x6-pdf-shipping-labels).

### Printing works locally but fails from production

Check production secret injection, outbound HTTPS access, configured printer IDs, and document URL accessibility. Do not solve a missing environment variable by hard-coding a production API key into source control.

Also verify that your production worker is actually running and consuming pending print tasks. A correctly written submission service cannot deliver tasks that never reach it.

## Start with one reliable document workflow

The simplest Spring Boot printing integration is a generated PDF, a known printer ID, and one authenticated HTTPS request. Reliability comes from adding durable task records, stable idempotency keys, status tracking, and a clear reprint process around that request.

[Create a free PrintScaler account](https://printscaler.com/signup), connect one printer, and test a non-sensitive PDF from your Java application. Once that path works end to end, move submission into your application's normal background workflow.

## Frequently asked questions

### Can Spring Boot print directly to a user's local printer?

A cloud-hosted Spring Boot application cannot automatically access a user's local printers through Java's printing APIs. Use a local agent connected to a printing API, or let the user download the PDF and print manually.

### Do I need a PDF library to print PDFs from Java?

You need a PDF generator if your application must create the document. If you already have a PDF, the PrintScaler integration can submit its bytes or URL without rendering it in Java. Direct printing through Java's local print services may require a PDF rendering library, depending on the print service.

### Can I print a PDF without opening a browser print dialog?

Yes. Spring Boot can submit the PDF directly to PrintScaler, which delivers it through the agent to the selected printer. Your backend must authorize the operation and choose the correct destination.

### What happens when the printer's computer is offline?

Jobs can queue while the agent computer is offline and print after it reconnects. They expire after a configurable time, so your application should monitor job status rather than assume every submission will eventually print.

### Should I use pdf_uri or pdf_base64 in a Spring Boot application?

Use pdf_base64 when the application already has the PDF bytes and you want to avoid hosting a retrievable file. Use pdf_uri when the PDF is available through a suitable URL, especially when avoiding base64 request and memory overhead is important. Consider document access and URL lifetime before choosing URL-based delivery.

### How do I avoid duplicate printing after a Java HTTP timeout?

Persist an idempotency key for each logical print operation and send it in the X-Idempotency-Key header. Reuse the same key when retrying that operation, because a timeout does not prove the first submission failed. Assign a new key only for an intentional new print operation.
