Skip to content

About

Official Java SDK (Java 11+) for the PDFik API — URL/HTML to PDF in one request (or queued with webhooks), screenshots, Markdown, Factur-X/ZUGFeRD e-invoices.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pdfik-client

Where this code lives: extracted from the PDFik platform monorepo (last sync 2026-10-20). Releases to Maven Central are cut from the monorepo; issues and PRs are welcome here.

Official Java SDK for PDFik — the URL/HTML-to-PDF API.

One call gets you the file: urlToPdfFile("https://example.com") holds the request while PDFik renders (up to 45 s) and returns the PDF bytes. For batches and long renders the asynchronous flow is there too: submit, get a job id back, and receive an HMAC-signed webhook (or poll) when the PDF is ready. Rendering runs on sandboxed headless Chromium, so modern CSS, web fonts and JavaScript-heavy pages come out the way they look in the browser.

Features

  • One-call rendering: the *File methods (urlToPdfFile, htmlToPdfFile, markdownToPdfFile, urlToImageFile, htmlToImageFile, einvoiceToPdfFile, each with an ...Async twin) send "wait": true and return the file bytes directly — no polling, no download step.
  • Modern HTTP: Built on java.net.http.HttpClient (Java 11+ built-in client) with zero extra HTTP library dependencies.
  • Fluent Builder: Native Builder pattern for client and options creation.
  • Sync & Async: Exposes both block-oriented synchronous methods and non-blocking asynchronous variants (via CompletableFuture).
  • Auto Retry: 5xx responses and network failures are retried with exponential backoff (3 attempts). A rate-limit or concurrency-limit 429 is waited out on every call — create, status and download — for the time the API states (up to 5 waits per call, each capped at 60 s), and so is a busy download (download-busy); every other 429 (quota, test-mode caps, an exhausted download counter) is thrown at once. Every create call carries an Idempotency-Key — yours if you pass one, otherwise a random one generated once per call and repeated each time that call is sent again — so a retried create cannot produce a second job. The *File methods are the carve-out: beyond the throttle waits they never resend — a 5xx, a network failure and in particular the 503 sync-timeout are thrown at once, because the one attempt may already have rendered (and charged) the job, and every automatic retry would be a full re-render. A timed-out sync job was cancelled, not charged, and its Idempotency-Key released, so whether to retry is your call.
  • Jackson Integrated: Uses standard jackson-databind for serialization/deserialization.

Installation

Maven

Add the following dependency to your pom.xml:

<dependency>
  <groupId>net.pdfik</groupId>
  <artifactId>pdfik-client</artifactId>
  <version>0.4.0</version>
</dependency>

Gradle

Add the following to your build.gradle:

implementation 'net.pdfik:pdfik-client:0.4.0'

Quick Start

One call, file back

urlToPdfFile sends "wait": true for you: the API holds the request while the page renders (up to 45 s) and answers with the PDF itself — nothing to poll, nothing to download:

import net.pdfik.PdfikClient;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) throws Exception {
        try (PdfikClient client = PdfikClient.builder()
                .apiKey("sk_live_...") // Retrieve from your dashboard
                .build()) {

            byte[] pdf = client.urlToPdfFile("https://example.com");
            Files.write(Path.of("output.pdf"), pdf);
            System.out.println("PDF saved to output.pdf (" + pdf.length + " bytes)");
        }
    }
}

Every creation endpoint has this one-call form — urlToPdfFile, htmlToPdfFile, markdownToPdfFile, urlToImageFile, htmlToImageFile (image bytes back), einvoiceToPdfFile — each also taking the full request object plus an optional Idempotency-Key, and each with an ...Async twin returning CompletableFuture<byte[]>. What to expect beyond the file, all thrown as PdfikException:

  • 503 SYNC_TIMEOUT — the render did not finish within the 45-second budget. The job was cancelled and not charged, and its Idempotency-Key was released, so retrying is safe — but the SDK deliberately never retries it for you (every retry is a full re-render). For renders this long, use the asynchronous flow below.
  • 422 carrying the job's own error code (PAGE_TIMEOUT, PAGE_NOT_FOUND, ...) via getErrorCode() — the render failed; the response body names the job_id.
  • A server from before synchronous mode ignores wait and answers 202 with the usual JSON; the *File methods detect that and throw an exception naming the job that was created, instead of handing JSON back as file bytes.

Two deliberate differences from the job-creating methods: 5xx responses and network failures are not resent (only the rate-limit/concurrency 429 waits remain — see Auto Retry above), and these requests run with a per-request timeout of at least 70 s whatever requestTimeout you configured, so the server's verdict always arrives before the client hangs up. The delivery (BYOB) option cannot be combined with wait and is rejected with IllegalArgumentException before any HTTP. The finished file also stays downloadable for the normal 24 h; its job id rides the X-PDFik-Job-Id response header.

Asynchronous flow: submit, poll (or webhook), download

For batches, webhook-driven pipelines and renders longer than 45 s:

import net.pdfik.PdfikClient;
import net.pdfik.models.*;
import java.io.FileOutputStream;
import java.time.Duration;

public class Main {
    public static void main(String[] args) {
        // Initialize client using Builder
        try (PdfikClient client = PdfikClient.builder()
                .apiKey("sk_live_...") // Retrieve from your dashboard
                .build()) {

            // Configure PDF styling and margins
            PdfOptions options = PdfOptions.builder()
                    .format(PaperFormat.A4)
                    .landscape(false)
                    .printBackground(true)
                    .margin(new MarginOptions("10mm", "10mm", "10mm", "10mm"))
                    .build();

            // 1. Submit the URL to be rendered
            JobCreatedResponse job = client.urlToPdf("https://example.com", options);
            System.out.println("Job created: " + job.getJobId() + ". Processing...");

            // 2. Poll until the job completes
            JobStatusResponse result = client.waitForJob(
                    job.getJobId(),
                    Duration.ofSeconds(120), // timeout
                    Duration.ofSeconds(2)    // poll interval
            );
            System.out.println("Job finished! Pages: " + result.getPagesCount());

            // 3. Download the PDF bytes
            byte[] pdfBytes = client.downloadPdf(job.getJobId());

            try (FileOutputStream fos = new FileOutputStream("output.pdf")) {
                fos.write(pdfBytes);
            }
            System.out.println("PDF saved to output.pdf");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Convert HTML to PDF (Async)

import net.pdfik.PdfikClient;
import net.pdfik.models.*;
import java.time.Duration;

public class Main {
    public static void main(String[] args) {
        PdfikClient client = PdfikClient.builder()
                .apiKey("sk_live_...")
                .build();

        client.htmlToPdfAsync("<h1>Hello World</h1><p>Sent from Java Async SDK</p>")
                .thenCompose(job -> {
                    System.out.println("Job created async: " + job.getJobId());
                    return client.waitForJobAsync(job.getJobId(), Duration.ofSeconds(120), Duration.ofSeconds(2))
                            .thenCompose(status -> {
                                System.out.println("Job complete. Downloading...");
                                return client.downloadPdfAsync(job.getJobId());
                            });
                })
                .thenAccept(pdfBytes -> {
                    System.out.println("Downloaded " + pdfBytes.length + " bytes.");
                    client.close();
                })
                .exceptionally(ex -> {
                    ex.printStackTrace();
                    client.close();
                    return null;
                })
                .join(); // wait: the SDK's threads are daemons, so main() must not return first
    }
}

Get the direct download URL

Get the direct API download URL for the generated PDF (requires the "X-API-Key" header to download):

JobFileResponse fileInfo = client.getFileUrl(job.getJobId());
System.out.println("Direct Download URL: " + fileInfo.getDownloadUrl());

Test mode

Pass test = true (dedicated urlToPdf(url, test) / htmlToPdf(html, test) overloads, also available on the full-parameter variants) to run a job through the full pipeline (statuses, webhook, download) without real rendering — the download returns a small sample PDF. Test jobs are free (no PDF or byte quota is debited) and rate-limited instead: 60 test calls per minute and 2,000 per day per account. Job status and webhook payloads always carry test: true|false, so you can safely exercise your integration end to end:

JobCreatedResponse job = client.urlToPdf("https://example.com", true);

JobStatusResponse result = client.waitForJob(job.getJobId());
System.out.println(result.getTest());      // true
System.out.println(result.getExpiresAt()); // e.g. 2026-08-05T12:00:00Z

byte[] pdfBytes = client.downloadPdf(job.getJobId()); // sample PDF

Markdown to PDF

markdownToPdf converts Markdown (CommonMark + GFM tables and strikethrough) with a built-in print stylesheet. Raw HTML inside the Markdown is escaped, not rendered — use htmlToPdf for full HTML control. The same PdfOptions (paper format, margins, header/footer, watermark, ...) and job flow apply. The Markdown may be up to 100,000 characters (longer input is rejected with 422); Markdown whose converted document is too large for the processing queue is rejected with 413 (payload-too-large-for-queue) and is not charged. Example:

// Simplest form
JobCreatedResponse job = client.markdownToPdf("# Report\n\n| Item | Price |\n| --- | --- |\n| Render | $0.01 |");

// Full control via the request object (+ optional Idempotency-Key)
MarkdownToPdfRequest request = new MarkdownToPdfRequest("# Report\n\nHello **world**");
request.setOptions(PdfOptions.builder().format(PaperFormat.A4).build());
job = client.markdownToPdf(request, "report-2026-001");

JobStatusResponse result = client.waitForJob(job.getJobId());
byte[] pdfBytes = client.downloadPdf(job.getJobId());

Screenshots

urlToImage / htmlToImage capture a page as a PNG (default) or JPEG instead of a PDF. ImageOptions: format ("png" | "jpeg"), fullPage (default false - the visible area only; with true the height follows the real page and is clipped at 8,192 px, which is a ceiling and not a target, so a 2,000 px page still gives a 2,000 px image, and horizontal overflow beyond the viewport width is never captured), quality (1-100, JPEG only) and viewport (ImageViewport(width, height); you pick the window size and we capture exactly that - nothing is scaled or fitted - width 320-1920, height 320-8192, defaults to 1024x768). Polling is identical to the PDF endpoints, and downloadPdf returns the raw image bytes (image/png or image/jpeg, filename {jobId}.png/.jpg):

UrlToImageRequest request = new UrlToImageRequest("https://example.com");
request.setOptions(ImageOptions.builder()
        .format("jpeg")
        .quality(80)
        .fullPage(true)
        .viewport(new ImageViewport(1280, 720))
        .build());
JobCreatedResponse job = client.urlToImage(request);

client.waitForJob(job.getJobId());
byte[] imageBytes = client.downloadPdf(job.getJobId()); // JPEG bytes

JobCreatedResponse shot = client.htmlToImage("<h1>Hello</h1>"); // 1024x768 PNG

UrlToImageRequest also accepts the Pro+ auth option (basic/bearer), exactly as urlToPdf. Passing quality together with PNG is rejected with 422. Async variants: markdownToPdfAsync, urlToImageAsync, htmlToImageAsync.

Deliver to your own bucket (BYOB)

Set delivery (every plan, on every request object — UrlToPdfRequest, HtmlToPdfRequest, MarkdownToPdfRequest, UrlToImageRequest, HtmlToImageRequest, EInvoiceToPdfRequest) and the output is uploaded straight to your own bucket via a presigned PUT URL; nothing is stored on PDFik's side. The URL must be https on the standard port 443 (any other port is rejected with 422); presign it for at least 15 minutes and without a Content-Type condition:

UrlToPdfRequest request = new UrlToPdfRequest();
request.setUrl("https://example.com");
request.setDelivery(new DeliveryOptions(presignedPutUrl)); // mode "presigned_put" is the only mode
JobCreatedResponse job = client.urlToPdf(request);

client.waitForJob(job.getJobId()); // "done" means the PUT to your bucket succeeded

Once a delivered job is done, the job.finished webhook reports the outcome only: it carries neither file_url nor expires_at, because PDFik keeps no copy and never records where the file went (the presigned URL is a credential, so it is used once and forgotten). You already know the destination — you signed it. downloadPdf answers 404 (output-delivered-externally) — the file only exists in your bucket. delivery cannot be combined with test mode (400).

Factur-X e-invoices

PDFik can produce hybrid e-invoices (Factur-X / ZUGFeRD): a PDF/A-3 file with your UN/CEFACT Cross-Industry-Invoice XML embedded as factur-x.xml. Two ways to get one:

1. From the XML alone (einvoiceToPdf) — PDFik builds the human-readable invoice from the XML with a block template (the account default, a saved dashboard template via templateId, or an inline template), renders it to PDF/A-3 and embeds the XML. The normal job flow applies afterwards (waitForJob + downloadPdf):

// Simplest form: default template, profile defaults to en16931
JobCreatedResponse job = client.einvoiceToPdf(ciiXml);

// Full control via the request class (+ optional Idempotency-Key)
EInvoiceToPdfRequest request = new EInvoiceToPdfRequest(ciiXml);
request.setProfile("en16931");    // must match the XML's guideline ID: minimum | basicwl | basic | en16931 | extended
request.setTemplateId("tpl_..."); // saved dashboard template; mutually exclusive with setTemplate(...)
job = client.einvoiceToPdf(request, "invoice-2026-001");

JobStatusResponse result = client.waitForJob(job.getJobId());
byte[] pdfA3 = client.downloadPdf(job.getJobId());

2. Attached to your own rendering (einvoice option on urlToPdf / htmlToPdf) — the page you render becomes the visual half of the hybrid; the output is normalized to PDF/A-3 with the XML embedded:

EInvoiceOptions einvoice = new EInvoiceOptions(ciiXml, "en16931");
JobCreatedResponse job = client.htmlToPdf(invoiceHtml, null, null, null, einvoice, null, null);

Notes:

  • The XML (UTF-8, up to 1 MB) is validated against the official XSD of the declared profile before any quota is spent; its GuidelineSpecifiedDocumentContextParameter must match the profile. TypeCode 380 (invoice) and 381 (credit note) are supported. Invalid XML returns 422 (einvoice-xml-invalid).
  • The output is validated with veraPDF and Mustangproject. Schema-valid does not mean tax-compliant — the invoice content remains your responsibility.
  • Profiles minimum and basicwl carry accompanying data only and are NOT a legally sufficient e-invoice; use basic, en16931 or extended for a full invoice.
  • The einvoice option is mutually exclusive with userPassword (PDF/A forbids encryption) and compression (re-saving breaks the PDF/A attributes).
  • Available on every plan, Free included — the output is the same clean PDF/A-3.

Limits, retention and error codes

  • Generated volume per month (byte quota, counts rendered output only — downloads are free): Free 0.5 GB, Starter 10 GB, Pro 50 GB, Business 300 GB. Business plans can purchase additional +1 GB blocks from the dashboard. Exceeding the quota returns 429 (quota-bytes-exceeded).
  • File size: up to 150 MB per PDF on every plan — contact support if you need more. A larger render fails the job with FILE_TOO_LARGE (file-too-large).
  • Retention: each file is available for download for 24 hours after the job finishes; the exact deadline is JobStatusResponse.getExpiresAt() (expires_at in the webhook payload). After that the download returns 410 (file-expired).
  • Downloads: at most 3 download attempts per file (counted when the stream starts); after that the API returns 429 (download-attempts-exhausted) — re-render the file or contact support. Only 1 download stream may be active at a time; a concurrent request returns 429 (download-busy), which does not burn an attempt — downloadPdf waits the ~10 seconds the API states and asks again by itself.

Handling Webhooks

If you specify webhookUrl in the options of urlToPdf or htmlToPdf, PDFik will send an HTTP POST request to your server when the rendering job is finished.

You should verify the authenticity of the webhook by validating the signature. We provide a static helper verifyWebhookSignature in PdfikClient for this purpose.

During a secret rotation both the new and the previous secret verify for the overlap window you pick in the dashboard (1 hour to 5 days, or none at all), so check the new one first and fall back to the old one while you roll your receivers out.

Custom delivery headers (e.g. a Cloudflare Access service token) are configured once on the dashboard Webhooks page and attached to every delivery — nothing to pass per request.

Example (Spring Boot):

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import net.pdfik.PdfikClient;
import java.util.Map;
import java.nio.charset.StandardCharsets;

@RestController
public class WebhookController {

    private static final String WEBHOOK_SECRET = "whsec_..."; // From PDFik dashboard

    @PostMapping("/webhook")
    public ResponseEntity<Map<String, Object>> handleWebhook(
            @RequestBody byte[] rawBody,
            @RequestHeader Map<String, String> headers) {

        boolean valid = PdfikClient.verifyWebhookSignature(
                new String(rawBody, StandardCharsets.UTF_8),
                headers,
                WEBHOOK_SECRET
        );

        if (!valid) {
            return ResponseEntity.status(401).body(Map.of("error", "Invalid signature"));
        }

        // Signature is valid. Process the event payload...
        // System.out.println("Webhook validated successfully!");

        return ResponseEntity.ok(Map.of("received", true));
    }
}

License

MIT License.

About

Official Java SDK (Java 11+) for the PDFik API — URL/HTML to PDF in one request (or queued with webhooks), screenshots, Markdown, Factur-X/ZUGFeRD e-invoices.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages