Skip to content

Guide · Java

Merge PDFs and add a password in Java

An invoice, the terms and a data sheet should reach the customer as one file, and it should not be readable by anyone who finds it. This Java program joins documents you already rendered and encrypts the result, in two calls and without a PDF library.

Free plan with 100 units a month, no credit card

What you need

  • An API key. Create one in the app under API keys and export it as FORMFEED_API_KEY. A test key (ff_test_…) is right for trying things out. See the quick start.
  • Java. You need Java 17 or newer; java.net.http.HttpClient and text blocks are part of the JDK.
  • The sources. sources.json next to the program: a JSON array of what goes into the document, in order. A render id (rnd_…) or the name of a PDF in the workspace library, two to fifty of them.
  • A password. In FORMFEED_PDF_PASSWORD. Generate one per document and send it through a different channel than the file.
  • A plan with PDF tools. The tools are part of Starter and above.

The program

The program reads the sources, merges them into one PDF, encrypts that result with the password and saves the finished file.

Bundle.javaJava
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.util.regex.Matcher;
import java.util.regex.Pattern;

public class Bundle {
  static final HttpClient CLIENT = HttpClient.newHttpClient();

  static String api(String path, String body) throws Exception {
    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1" + path))
        .header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();
    HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 400) {
      System.err.println(response.body());
      System.exit(1);
    }
    return response.body();
  }

  /** The value of a top-level JSON field; a project would use Jackson or Gson instead. */
  static String field(String json, String name) {
    Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*(?:\"([^\"]*)\"|([^,}\\s]+))").matcher(json);
    return m.find() ? (m.group(1) != null ? m.group(1) : m.group(2)) : "";
  }

  public static void main(String[] args) throws Exception {
    String sources = Files.readString(Path.of("sources.json")).strip();

    // 1. join the documents, in the order they are given
    String merged = api("/pdf/merge", "{\"sources\": %s, \"filename\": \"bundle.pdf\"}".formatted(sources));

    // 2. encrypt the result; printing stays allowed, copying and editing do not
    String protectedPdf = api("/pdf/protect", """
        {"source": "%s", "user_password": "%s", "permissions": ["print"]}
        """.formatted(field(merged, "id"), System.getenv("FORMFEED_PDF_PASSWORD")));

    CLIENT.send(HttpRequest.newBuilder(URI.create(field(protectedPdf, "download_url"))).build(),
        HttpResponse.BodyHandlers.ofFile(Path.of("bundle.pdf")));
    System.out.println(field(protectedPdf, "id") + " " + field(protectedPdf, "page_count") + " page(s), " + field(protectedPdf, "units") + " unit(s)");
  }
}

Run it

It prints the render’s id, the page count and the units, and bundle.pdf lies next to the program. Opening it asks for the password; printing works, copying text does not.

Shell
export FORMFEED_API_KEY=ff_test_...
export FORMFEED_PDF_PASSWORD=$(openssl rand -base64 18)
java Bundle.java

What is particular about Java

  • No JSON library in the JDK. To stay free of dependencies the body is a text block, and the values the program needs from JSON (the render’s id and download_url, an invoice number) are read with a regular expression. In a project, build the body and read the answer with Jackson or Gson.
  • Single-file launch. java Render.java compiles and runs in one step, so trying it needs no build tool.
  • Straight to disk. BodyHandlers.ofFile streams the PDF into the file without holding it in memory.

Why two calls

  • Order matters. An encrypted PDF cannot be changed any more, so the password comes last. Merging an already protected document fails.
  • Every step is a render. Merge and protect each produce a render of their own with an id, a download URL and a line in the render log; the second takes the first’s id as its source.
  • Half a unit each. A tool costs 0.5 units on a live key, whatever the page count. The renders that produced the sources were counted when they ran.
  • Or in one request. A render can carry the same steps as post and do everything in one call. Use the tools when the documents already exist, and post when you are rendering anyway. See PDF tools.

What the permissions do

Everything not listed in permissions is forbidden (leave the list out and all four of print, copy, modify and annotate are allowed): with ["print"] a reader may print but not copy text, edit or annotate. Without a user_password anyone can open the document and the permissions still apply, which is the right setting for a document that is public but should not be edited. An owner_password lifts the restrictions; a random one is used when you send none. PDF viewers honour the permissions, but they are no protection against someone who can open the file: confidentiality comes from the user_password.

When the API says no

  • One shape for every error. Errors are problem documents (RFC 9457) with type, title, status, detail, a stable code and the request_id to quote to support; the type URL leads to a page about that error. The list is in the error reference.
  • The usual ones. 401 unauthorized for a missing or revoked key, 404 template_not_found for a wrong template name, 422 template_runtime_error when the template fails on the data, 429 rate_limited above the plan’s requests per second, 402 quota_exceeded when the Free plan’s units are used up (a paid plan bills extra units up to its hard cap).
  • Retry safely. Send an Idempotency-Key header with the POST, and repeat the request with the same key when you do not know whether the first attempt arrived: for 24 hours the key returns the first answer instead of rendering and billing again. The answer is stored once the first attempt has been answered, so do not retry while it may still run; and problems below 500 are stored too, so a corrected request needs a new key.

The job around this program

This guide is one program. What the job needs around it, from the template to the limits and the price, is on the page Contract and agreement PDFs.

More Java guides

FAQ

Where do the render ids come from?
From the renders you made earlier: the id of every answer. They must belong to the same workspace and their output must still exist. Files you uploaded to the library are named instead.
Which encryption is used?
AES-256, done by qpdf. The password never appears in the render log, and it is not stored: lose it and the document cannot be opened.
Can I watermark as well?
Yes, /pdf/watermark is the third tool and takes a text or an image. Chain it before the password, like the merge. See PDF tools.

Merge and protect in other languages

Start rendering today

The free plan includes 100 units a month and free test renders. Change plans when you need more.