---
seo_title: Java File Conversion SDK: Maven Central API Client | EnConvert
meta_desc: Official EnConvert Java SDK for Java 17+. Add one Maven or Gradle dependency to convert files and to perceive, discover, distill, ingest, and watch web pages.
keywords: java file conversion sdk, convert files java, maven file conversion api client, gradle file conversion library, url to pdf java, docx to pdf java, html to pdf java, heic to webp java, java web scraping api, web page to markdown java, java rag ingestion pipeline, enconvert java sdk
---

# Java File Conversion SDK

`com.enconvert:enconvert-sdk` is the official Java client for the EnConvert API. One Maven or Gradle dependency gives you file conversion (URL to PDF, DOCX to PDF, HEIC to WebP, anything to Markdown) plus the V2 web intelligence surface: perceive, discover, lookup, distill, ingest, and watch. It targets Java 17 and above, runs on the JDK's built in `java.net.http.HttpClient`, and pulls in Gson as its only third party dependency. Every call is a plain blocking method that returns a typed record, and long conversions transparently recover from reverse proxy timeouts by polling job status.

<div class="alert alert-info">
<strong>Maven Central:</strong> <code>com.enconvert:enconvert-sdk:0.0.1</code> · <strong>Source:</strong> <a href="https://github.com/conversionapi/java-sdk">conversionapi/java-sdk</a> · <strong>Java:</strong> 17+ · <strong>Dependencies:</strong> Gson only
</div>

---

## Install

```groovy
// build.gradle
dependencies {
    implementation 'com.enconvert:enconvert-sdk:0.0.1'
}
```

```kotlin
// build.gradle.kts
dependencies {
    implementation("com.enconvert:enconvert-sdk:0.0.1")
}
```

```xml
<!-- pom.xml -->
<dependency>
    <groupId>com.enconvert</groupId>
    <artifactId>enconvert-sdk</artifactId>
    <version>0.0.1</version>
</dependency>
```

HTTP is handled by `java.net.http.HttpClient` from the JDK. The only third party artifact that comes along is [Gson](https://github.com/google/gson) for JSON, declared as an `api` dependency so it is visible on your compile classpath.

---

## Quick start

```java
import com.enconvert.Enconvert;
import com.enconvert.model.ConversionResult;
import com.enconvert.model.UrlToPdfOptions;
import com.enconvert.model.v2.PerceiveOptions;
import com.enconvert.model.v2.PerceiveResult;

import java.util.List;

Enconvert client = new Enconvert(System.getenv("ENCONVERT_API_KEY"));

ConversionResult pdf = client.convertUrlToPdf("https://example.com",
        UrlToPdfOptions.builder().saveTo("page.pdf").build());
System.out.println(pdf.presignedUrl());

// Read a page the way your agent should, with a quality score attached.
PerceiveResult page = client.v2.perceive("https://example.com",
        PerceiveOptions.builder().outputs(List.of("markdown", "structured")).build());
System.out.println(page.outputs().get("markdown").url());
System.out.println(page.renderQuality());   // e.g. 0.93
```

Every options class is an immutable builder and every response is a Java `record`, so accessors read as `pdf.presignedUrl()` and `page.renderQuality()`. The client holds one shared `HttpClient` and no mutable per-request state, so a single instance can be a singleton or a Spring bean shared across threads. Snippets below omit imports: options and response types live in `com.enconvert.model` (conversion) and `com.enconvert.model.v2` (web intelligence), exceptions in `com.enconvert.exceptions`.

---

## What the client exposes

`Enconvert` carries the conversion surface directly. The web intelligence surface lives on the public final field `client.v2`, an instance of `EnconvertV2`.

| Group | Methods | Returns |
|-------|---------|---------|
| Single URL | `convertUrlToPdf`, `convertUrlToScreenshot`, `convertUrlToMarkdown` | `ConversionResult` |
| File upload | `convertImage`, `convertDocument`, `convertToMarkdown`, `convertToPdf` | `ConversionResult` |
| Whole site | `convertWebsiteToPdf`, `convertWebsiteToScreenshot` | `BatchSubmission` |
| Status | `getJobStatus`, `getBatchStatus`, `waitForBatch` | `JobStatus`, `BatchStatus` |
| `v2` perceive | `perceive`, `perceiveDirect`, `getPerceiveOperation`, `perceiveBatch`, `getPerceiveBatch`, `downloadPerceiveArtifact` | `PerceiveResult`, `PerceiveDirectResult`, `PerceiveBatchResult` |
| `v2` discover | `discover` | `DiscoverResult` |
| `v2` lookup | `lookup` | `LookupResult` |
| `v2` distill | `distill` | `DistillResult` |
| `v2` ingest | `ingest`, `ingestFiles`, `getIngestJob`, `listIngestJobs`, `cancelIngestJob`, `retryIngestWebhook`, `getWebhookSecret`, `rotateWebhookSecret` | `IngestJob`, `IngestJobList`, `WebhookRetryResult`, `WebhookSecret` |
| `v2` watch | `createWatcher`, `getWatcher`, `listWatchers`, `getWatcherSnapshots`, `updateWatcher`, `deleteWatcher` | `Watcher`, `WatcherList`, `WatcherSnapshotList` |

Most methods have a short overload with no options argument, so `client.v2.perceive(url)` and `client.convertUrlToPdf(url)` both compile. `convertImage`, `distill`, and `ingest` are the exceptions: each always takes its options object, because the target format, the schema, and the source are required respectively.

---

## File conversion

The conversion endpoints cover 43 implemented `{input}-to-{output}` pairs, two auto detecting endpoints (`anything-to-markdown` and `anything-to-pdf`), and the browser rendering endpoints. The full parameter reference is in [Parameters and options](/docs/parameters-options).

### convertUrlToPdf

Render any public URL to PDF.

```java
ConversionResult result = client.convertUrlToPdf("https://example.com",
        UrlToPdfOptions.builder()
                .pdfOptions(PdfOptions.builder().pageSize("A4").orientation("landscape").build())
                .singlePage(false)
                .viewportWidth(1440)
                .saveTo("report.pdf")
                .build());
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `saveTo` | `String` | none | Local path to write the PDF to. Parent directories are created for you. |
| `singlePage` | `boolean` | `true` | `true` produces one continuous page. `false` paginates using `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions` | none | Page size, orientation, margins, scale, grayscale, header, footer. See [PDF options](#pdf-options). |
| `viewportWidth` | `int` | `1920` | Browser viewport width in pixels. |
| `viewportHeight` | `int` | `1080` | Browser viewport height in pixels. |
| `loadMedia`, `enableScroll` | `boolean` | `true` | Wait for images and video before capture, and scroll top to bottom to trigger lazy loaders. |
| `outputFilename` | `String` | auto | Override the generated filename. |
| `auth`, `cookies`, `headers` | `HttpBasicAuth`, `List<BrowserCookie>`, `Map<String, String>` | none | Credentials, injected cookies, and extra request headers for pages behind a login. |

### convertUrlToScreenshot

Capture a PNG of any URL. Same viewport, media, scroll, filename, auth, cookie, and header options as `convertUrlToPdf`, minus `singlePage` and `pdfOptions`.

```java
client.convertUrlToScreenshot("https://example.com",
        UrlToScreenshotOptions.builder().viewportWidth(1440).saveTo("screenshot.png").build());
```

### convertUrlToMarkdown

Extract clean GitHub Flavored Markdown from a URL. Navigation, footers, ads, and scripts are stripped, the main article body is kept, and YAML frontmatter (title, description, url, links, images) is prepended. Same option set as `convertUrlToScreenshot`.

```java
client.convertUrlToMarkdown("https://example.com/article",
        UrlToMarkdownOptions.builder().saveTo("article.md").build());
```

### convertImage

Convert between `jpeg`, `png`, `svg`, `heic`, and `webp`, or rasterize a PDF to JPEG.

```java
// From a path on disk
client.convertImage(Path.of("photo.heic"),
        ConvertImageOptions.builder("webp").saveTo("photo.webp").build());

// Rasterize a PDF
client.convertImage(Path.of("scan.pdf"),
        ConvertImageOptions.builder("jpeg").saveTo("scan.jpeg").build());

// From in-memory bytes with an explicit filename
byte[] bytes = Files.readAllBytes(Path.of("photo.heic"));
client.convertImage(new FileInput(bytes, "photo.heic"),
        ConvertImageOptions.builder("webp").build());
```

Three input overloads exist on every file method: `java.nio.file.Path` (read from disk), raw `byte[]` (the filename defaults to `upload.bin`), and `com.enconvert.FileInput` when you need to pair in-memory bytes with a real filename. The input format is resolved from the extension; the output format is required.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `outputFormat` | `String` | Yes | Passed to `ConvertImageOptions.builder(outputFormat)`. One of `jpeg`, `png`, `svg`, `heic`, `webp`. Aliases such as `jpg` are normalized. |
| `saveTo` | `String` | no | Local path to write the result to. |
| `outputFilename` | `String` | no | Override the generated filename. |

### convertDocument

Convert documents and structured data formats. The output format defaults to `pdf`.

```java
// docx to pdf
client.convertDocument(Path.of("report.docx"),
        ConvertDocumentOptions.builder().saveTo("report.pdf").build());

// json to yaml
client.convertDocument(Path.of("data.json"),
        ConvertDocumentOptions.builder().outputFormat("yaml").saveTo("data.yaml").build());

// markdown to pdf with page setup
client.convertDocument(Path.of("README.md"),
        ConvertDocumentOptions.builder()
                .outputFormat("pdf")
                .pdfOptions(PdfOptions.builder()
                        .pageSize("A4")
                        .margins(new PdfMargins(20.0, 20.0, 25.0, 25.0))
                        .build())
                .saveTo("readme.pdf")
                .build());
```

**Recognized input extensions:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

EPUB has no dedicated document pair. Send `.epub` files through [`convertToPdf`](#converttopdf) or [`convertToMarkdown`](#converttomarkdown) instead.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `outputFormat` | `String` | `"pdf"` | Target format. |
| `saveTo` | `String` | none | Local path to write the result to. |
| `outputFilename` | `String` | none | Override the generated filename. |
| `pdfOptions` | `PdfOptions` | none | Page setup, honored when the output is PDF. |

### Supported conversions

`convertImage` and `convertDocument` validate the `{input}-to-{output}` pair against the endpoints the API actually implements. An unsupported pair throws `IllegalArgumentException` immediately, listing the valid outputs for that input, instead of paying a round trip for a request that cannot succeed.

| Input | Outputs |
|-------|---------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `yaml` | `json` |
| `csv` | `json`, `xml` |
| `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | each other, all 20 pairs |
| `pdf` | `jpeg` |

The same table is queryable at runtime through `com.enconvert.Formats`: `Formats.validOutputsFor("json")` returns `[csv, toml, xml, yaml]`, `Formats.validOutputsFor("pdf")` returns `[jpeg]`, and `Formats.IMPLEMENTED_CONVERSIONS` holds all 43 endpoint names.

### convertToMarkdown

Send any supported document through one auto detecting endpoint and get clean Markdown back. The heading hierarchy survives, which makes this a natural first stage for a RAG pipeline.

```java
client.convertToMarkdown(Path.of("handbook.docx"),
        ConvertToMarkdownOptions.builder().saveTo("handbook.md").build());
```

Accepts PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats. The format is detected server side, so there is no client side extension check: any file is uploaded as-is. Images are not supported and are rejected with `400`. The only options are `saveTo` and `outputFilename`.

### convertToPdf

The other auto detecting endpoint: almost anything to PDF.

```java
// pptx to pdf
client.convertToPdf(Path.of("slides.pptx"),
        ConvertToPdfOptions.builder().saveTo("slides.pdf").build());

// pdf passthrough, converted to grayscale
client.convertToPdf(Path.of("scan.pdf"),
        ConvertToPdfOptions.builder()
                .pdfOptions(PdfOptions.builder().grayscale(true).build())
                .saveTo("scan-gray.pdf")
                .build());
```

Accepts office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, and an existing PDF as a passthrough. EPUB is handled here because it has no dedicated document pair. Options are `saveTo`, `outputFilename`, and `pdfOptions`.

<div class="alert alert-warning">
<strong>Only <code>grayscale</code> is honored on this endpoint.</strong> Page geometry (page size, width and height, orientation, margins, scale, header, footer) is ignored by <code>anything-to-pdf</code>. When you need full page setup, go through <code>convertDocument</code> or <code>convertUrlToPdf</code> instead.
</div>

### Whole site conversion

`convertWebsiteToPdf` and `convertWebsiteToScreenshot` discover every page of a site, convert each one in the background, and bundle the results into a single ZIP. Both are asynchronous and return a `BatchSubmission`. Both require a private API key.

```java
BatchSubmission batch = client.convertWebsiteToPdf("https://example.com",
        WebsiteToPdfOptions.builder()
                .crawlMode("sitemap")                      // "auto" (default), "sitemap", "full"
                .excludePatterns(List.of("/blog/tag/"))    // full crawl mode only
                .notificationEmail("ops@example.com")
                .build());

System.out.println(batch.batchId() + " " + batch.urlCount() + " " + batch.discoveryMethod());

// Block until the batch leaves "processing", then save the ZIP
BatchStatus status = client.waitForBatch(batch.batchId(),
        WaitForBatchOptions.builder().saveTo("site.zip").build());
System.out.println(status.completed() + " of " + status.total() + " pages converted");
```

`convertWebsiteToScreenshot` works identically and produces a ZIP of PNGs. `waitForBatch` polls every 5 seconds by default, gives up after 30 minutes, and accepts `intervalMs`, `timeoutMs`, and `saveTo`. On timeout it throws `ApiException` with status `504`.

### Polling status yourself

```java
JobStatus job = client.getJobStatus("job_abc123");
if ("success".equals(job.status())) System.out.println(job.presignedUrl());
if ("failed".equals(job.status())) System.err.println(job.error());
BatchStatus batch = client.getBatchStatus("bat_abc123");
if (!"processing".equals(batch.status())) System.out.println(batch.zipDownloadUrl());
```

---

## Web intelligence (V2)

Everything under `client.v2` turns web pages into agent ready data. Every read carries `renderQuality`, a score from 0.0 to 1.0 that says how cleanly the page actually rendered. A challenge page, a cookie wall, or an empty SPA shell comes back with a low score and populated `warnings()` and `deductions()` rather than being passed off as real content, so a bad read never quietly enters your agent's context. The content is still returned; it is just flagged. Background on the model is in the [V2 overview](/docs/v2-overview).

### Perceive

Render one URL into the artifacts you ask for. Endpoint reference: [Perceive](/docs/v2-perceive).

```java
PerceiveResult page = client.v2.perceive("https://example.com",
        PerceiveOptions.builder()
                .outputs(List.of("markdown", "screenshot", "structured"))
                .extract(List.of("tables", "metadata"))
                .waitFor("css:main")
                .build());

System.out.println(page.renderQuality());                 // 0.0 to 1.0
System.out.println(page.statusCode() + " " + page.deductions());  // e.g. 200 {http_error=0.7}
System.out.println(page.outputs().get("markdown").url()); // signed URL, 15 minutes
System.out.println(page.structured());                    // caller-defined shape
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `outputs` | `List<String>` | `["markdown", "structured"]` | Any of `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `List<String>` | none | Heuristic targets: `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `schema` | `Map<String, Object>` | none | JSON schema for structured extraction. |
| `waitFor`, `waitTimeoutMs` | `String`, `int` | none, `30000` | A CSS selector, optionally prefixed `css:`, or `js:<expr>` to await, with a budget of 0 to 60000 ms. |
| `jsCode` | `String` | none | JavaScript run after navigation, max 20000 characters. |
| `viewport`, `mobile` | `PerceiveViewport`, `boolean` | 1920 x 1080, `false` | Width 320 to 3840, height 240 to 2160, or mobile emulation. |
| `onlyMainContent` | `boolean` | `true` | Strip nav, header, footer, and cookie banners from the Markdown artifact and the `main_content` extract. |
| `cacheMode` | `String` | `"enabled"` | `enabled` reuses a 1 hour cache, `bypass` skips it, `refresh` forces a re-render. |
| `blockResources` | `List<String>` | none | Resource types the browser should not load, for example `image`, `font`, `script`. |
| `pdfOptions` | `PdfOptions` | none | Only meaningful when `outputs` contains `pdf`. |
| `headers`, `cookies`, `auth` | `Map`, `List<BrowserCookie>`, `HttpBasicAuth` | none | Request headers, injected cookies, HTTP Basic credentials. |
| `respectRobots` | `boolean` | none | Honor the site's robots rules. |

<div class="alert alert-warning">
<strong>Not wired up yet.</strong> <code>proxyUrl</code>, <code>geolocation</code>, and <code>actionChain</code> exist on the builder but are not available server side and are currently rejected with <code>422</code>.
</div>

Artifact URLs are signed for 15 minutes and re-signed on every read of the operation, so `client.v2.getPerceiveOperation(page.operationId())` hands you fresh links. Batch up to 1000 URLs with one shared options block: small batches complete inline, larger ones come back with status `queued`, so poll them.

```java
PerceiveBatchResult batch = client.v2.perceiveBatch(
        List.of("https://a.example.com", "https://b.example.com"),
        PerceiveBatchOptions.builder()
                .outputs(List.of("markdown"))
                .outputMode("zip")          // "manifest" (default) or "zip"
                .build());

PerceiveBatchResult done = client.v2.getPerceiveBatch(batch.jobId());
System.out.println(done.completed() + "/" + done.total());
```

Skip the signed URL round trip entirely with `perceiveDirect`, which streams the artifact bytes back. It requires exactly one artifact producing output (anything except `structured`) and throws `IllegalArgumentException` before sending if you ask for more or fewer:

```java
PerceiveDirectResult direct = client.v2.perceiveDirect("https://example.com",
        PerceiveOptions.builder().outputs(List.of("pdf")).build());

Files.write(Path.of(direct.filename()), direct.content());
System.out.println(direct.renderQuality() + " " + direct.contentType());

// Re-download a stored artifact of an earlier operation
PerceiveDirectResult stored = client.v2.downloadPerceiveArtifact(page.operationId(), "markdown");
```

`downloadPerceiveArtifact` accepts a null or omitted output name when the operation produced exactly one artifact, otherwise it returns `400` listing the available outputs. Once the stored artifact has aged out it returns `410`.

### Discover

Enumerate a site's URLs without rendering anything. No browser is involved, so it is fast. Endpoint reference: [Discover](/docs/v2-discover).

```java
DiscoverResult found = client.v2.discover("https://example.com",
        DiscoverOptions.builder()
                .mode("hybrid")                        // "sitemap", "crawl", "hybrid"
                .maxUrls(200)
                .maxDepth(3)
                .excludePatterns(List.of("/tag/"))
                .build());

System.out.println(found.total() + " urls, truncated=" + found.truncated());
found.urls().forEach(System.out::println);
```

| Option | Type | Default | Range |
|--------|------|---------|-------|
| `mode` | `String` | `"hybrid"` | `sitemap`, `crawl`, `hybrid` |
| `maxUrls` | `int` | `100` | 1 to 1000 |
| `maxDepth` | `int` | `2` | 1 to 5 |
| `includePatterns`, `excludePatterns` | `List<String>` | none | Regex allowlist and denylist, max 50 each. The denylist is applied second. |
| `sameDomainOnly`, `respectRobots` | `boolean` | `true`, none | Stay on the seed domain, and honor the site's robots rules. |

### Lookup

Categorized web search, optionally auto rendering the top results. Endpoint reference: [Lookup](/docs/v2-lookup).

```java
LookupResult search = client.v2.lookup("best static site generators",
        LookupOptions.builder()
                .category("web")        // web, news, images, scholar, patents, maps
                .numResults(10)
                .country("us")
                .timeFilter("month")    // hour, day, week, month, year
                .perceiveTop(3)         // auto-render the top 3 hits
                .build());

search.results().forEach(hit -> {
    System.out.println(hit.position() + " " + hit.title() + " " + hit.url());
    if (hit.perceive() != null) System.out.println("  quality " + hit.perceive().renderQuality());
});
```

With `perceiveTop` above 0 (0 to 10, default 0), the top N result URLs are rendered through perceive and each hit carries its full `PerceiveResult` inline on `hit.perceive()`. `numResults` runs 1 to 100 and defaults to 10; `page` runs 1 to 10.

### Distill

Schema driven structured extraction across one or more pages. Endpoint reference: [Distill](/docs/v2-distill).

```java
DistillResult extraction = client.v2.distill(
        DistillOptions.builder(Map.of("products", "list of product names with their listed price"))
                .urls(List.of("https://example.com/catalog"))
                .cssSchema(CssSchema.builder(".product-card", List.of(
                                CssField.builder("name", "text").selector("h3").build(),
                                CssField.builder("price", "text").selector(".price").build()))
                        .targetField("products")
                        .build())
                .build());

extraction.results().forEach(item ->
        System.out.println(item.data() + " via " + item.extractionTier()));
```

The optional `cssSchema` runs a free CSS pass first; only the fields it cannot answer escalate to the LLM tier, and `item.extractionTier()` reports which path produced the record (`css`, `llm`, `mixed`, or `none`). You can also discover the URLs first instead of listing them:

```java
client.v2.distill(
        DistillOptions.builder(Map.of("title", "page title", "summary", "one-line summary"))
                .discoverFrom(new DistillDiscoverFrom("https://example.com", "sitemap", 10))
                .build());
```

Exactly one of `urls` (max 50) and `discoverFrom` must be set, and `schema` is required by the builder factory. Both rules are checked client side and throw `IllegalArgumentException` before any request goes out.

### Ingest

Turn a whole site, or a set of uploaded documents, into chunked RAG ready JSONL through one pipeline. Ingest is always asynchronous. Endpoint reference: [Ingest](/docs/v2-ingest).

```java
// From a site
IngestJob job = client.v2.ingest(IngestOptions.builder()
        .mode("sitemap")                                  // "urls" (default), "sitemap", "crawl"
        .url("https://docs.example.com")
        .maxPages(100)
        .chunk(new IngestChunkOptions(512, 1))            // maxWords, sentenceOverlap
        .webhookUrl("https://my.app/hooks/enconvert")
        .build());

// Or from uploaded files
IngestJob fileJob = client.v2.ingestFiles(
        List.of(new FileInput(Files.readAllBytes(Path.of("handbook.pdf")), "handbook.pdf"),
                new FileInput(Files.readAllBytes(Path.of("notes.docx")), "notes.docx")),
        IngestFilesOptions.builder().chunk(new IngestChunkOptions(512, 1)).build());

// Poll for the JSONL
IngestJob status = client.v2.getIngestJob(job.jobId());
if ("completed".equals(status.status())) {
    System.out.println(status.outputUrl() + " (" + status.totalChunks() + " chunks)");
}

client.v2.listIngestJobs(V2ListOptions.builder().limit(20).build());
client.v2.cancelIngestJob(job.jobId());   // idempotent
```

`ingestFiles` accepts PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats. Chunking defaults to 512 words (32 to 4000) with 1 sentence of overlap (0 to 10). `mode` defaults to `urls`, which requires a non-empty `urls` list and forbids `url`; every other mode requires a seed `url` and forbids `urls`. The SDK enforces that pairing before sending.

Completion webhooks are HMAC signed. Fetch the secret and the header names you need to verify a delivery, rotate it when it leaks, and re-fire a delivery your endpoint missed:

```java
WebhookSecret secret = client.v2.getWebhookSecret();
System.out.println(secret.signatureHeader() + " " + secret.signatureScheme());

client.v2.rotateWebhookSecret();          // old signatures stop verifying immediately

WebhookRetryResult retry = client.v2.retryIngestWebhook(job.jobId());
System.out.println(retry.delivered() + " after " + retry.attempts() + " attempts");
```

### Watch

Recurring change monitoring on a URL, with email and webhook notification. Endpoint reference: [Watch](/docs/v2-watch).

```java
Watcher watcher = client.v2.createWatcher("https://example.com/pricing",
        WatchCreateOptions.builder()
                .frequencyMinutes(60)      // 60 to 43200, hourly floor
                .diffMode("auto")          // auto, text, structured, tables, metadata
                .webhookUrl("https://my.app/hooks/changes")
                .notifyEmail(true)
                .build());

WatcherSnapshotList history = client.v2.getWatcherSnapshots(watcher.watcherId(),
        SnapshotListOptions.builder().limit(10).build());
history.snapshots().forEach(s ->
        System.out.println(s.checkedAt() + " changed=" + s.hasChanges()
                + " similarity=" + s.similarity()));

client.v2.updateWatcher(watcher.watcherId(), WatcherUpdate.builder().status("paused").build());
client.v2.updateWatcher(watcher.watcherId(), WatcherUpdate.builder().webhookUrl("").build());
client.v2.deleteWatcher(watcher.watcherId());   // soft delete, idempotent
```

`listWatchers()` and `getWatcher(watcherId)` read them back. `updateWatcher` requires at least one field and throws `IllegalArgumentException` otherwise. An explicit empty string for `webhookUrl` clears the webhook, while leaving it null means "no change". `deleteWatcher` is a soft delete: it returns the tombstoned watcher with status `deleted`, and a deleted watcher then reads as `404`.

<div class="alert alert-warning">
<strong>Snapshot diffs contain untrusted page content.</strong> <code>WatcherSnapshot.changes()</code> is raw text lifted from the monitored page. Escape it before rendering it in a dashboard, an email, or a chat message.
</div>

---

## PDF options

`PdfOptions` is shared by `convertUrlToPdf`, `convertWebsiteToPdf`, `convertDocument`, `convertToPdf`, and `PerceiveOptions.pdfOptions`.

```java
client.convertUrlToPdf("https://internal.example.com/report",
        UrlToPdfOptions.builder()
                .pdfOptions(PdfOptions.builder()
                        .pageSize("A4")
                        .orientation("landscape")
                        .margins(new PdfMargins(10.0, 10.0, 15.0, 15.0))
                        .scale(0.9)
                        .header(new PdfHeaderFooter("Quarterly Report", 15.0))
                        .footer(new PdfHeaderFooter("Confidential", 12.0))
                        .build())
                .auth(new HttpBasicAuth("user", "pass"))
                .cookies(List.of(BrowserCookie.builder("session", "abc123").domain("internal.example.com").build()))
                .headers(Map.of("X-Tenant", "acme"))
                .saveTo("report.pdf")
                .build());
```

| Field | Type | Description |
|-------|------|-------------|
| `pageSize` | `String` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"`, and similar. |
| `pageWidth`, `pageHeight` | `double` | Custom dimensions. Set together, they override `pageSize`. |
| `orientation` | `String` | `"portrait"` or `"landscape"`. |
| `margins` | `PdfMargins` | Record of `top`, `bottom`, `left`, `right`. Any null field is omitted. |
| `scale` | `double` | Render scale, for example `0.9` for 90 percent. |
| `grayscale` | `boolean` | Post-process the PDF to grayscale. |
| `header`, `footer` | `PdfHeaderFooter` | Record of `content` (max 2000 characters) and `height`. |

`BrowserCookie` needs a name and value plus either `domain` or `url`; when `domain` is set without `path`, the API defaults `path` to `/`. Do not combine `auth` with an explicit `Authorization` header, because the API rejects the conflict.

---

## Error handling

Every SDK exception extends `EnconvertException`, which extends `RuntimeException`, so nothing forces a `throws` clause on your call sites. Catch the specific subclasses first.

```java
try {
    client.convertUrlToPdf("https://example.com");
} catch (AuthenticationException e) {
    System.err.println("Invalid or missing API key");
} catch (QuotaException e) {
    System.err.println("Request refused with 402: " + e.getMessage());
} catch (RateLimitException e) {
    System.err.println("Too many requests, back off and retry");
} catch (ApiException e) {
    System.err.println("API error [" + e.getStatusCode() + "]: " + e.getMessage());
}
```

| Class | Raised on | Status code |
|-------|-----------|-------------|
| `AuthenticationException` | Missing, invalid, or non-permitted API key | `401`, `403` |
| `QuotaException` | HTTP 402 | `402` |
| `RateLimitException` | Too many requests | `429` |
| `ApiException` | Any other 4xx or 5xx response | the actual code |
| `EnconvertException` | Base class, also raised on transport failure, an interrupted request, or an unreadable input file | none |

Client side validation (an unsupported conversion pair, a missing distill schema, an empty watcher update, the wrong number of outputs for `perceiveDirect`) throws `IllegalArgumentException` before any request is made. The message map for server responses is in the [Error codes](/docs/error-codes) reference.

---

## Timeout recovery

Long URL renders and large document conversions can outlive the reverse proxy timeout even when the conversion itself eventually succeeds. The SDK handles that transparently:

1. Before each single URL or file upload request, the SDK generates a UUID and sends it as `job_id`.
2. If the request comes back 5xx, the SDK switches to polling `GET /v1/convert/status/{jobId}` every 3 seconds.
3. On `success` it returns the result. On `failed` it throws `ApiException` carrying the server's error message.
4. The polling deadline is 5 minutes. Past that it throws `ApiException(504, "Conversion timed out")`.

You write no code for this. If a successful response omits `job_id`, the SDK backfills the id it generated, so `result.jobId()` is always usable with `getJobStatus`.

<div class="alert alert-info">
<strong>Website batch submissions are excluded on purpose.</strong> <code>convertWebsiteToPdf</code> and <code>convertWebsiteToScreenshot</code> have no per-job row to poll, so a 5xx there means the submission itself failed and is surfaced directly. V2 endpoints do not use job polling either; their asynchronous flows go through <code>getPerceiveBatch</code> and <code>getIngestJob</code>.
</div>

---

## Configuration

```java
Enconvert client = Enconvert.builder(System.getenv("ENCONVERT_API_KEY"))
        .baseUrl("https://api.enconvert.com")
        .timeout(Duration.ofSeconds(300))
        .build();
```

Three constructors are available as shorthand: `new Enconvert(apiKey)`, `new Enconvert(apiKey, baseUrl)`, and `new Enconvert(apiKey, baseUrl, timeout)`.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `String` | required | Private API key. A null or empty value throws `IllegalArgumentException`. |
| `baseUrl` | `String` | `https://api.enconvert.com` | API base URL. Trailing slashes are stripped. |
| `timeout` | `Duration` | 300 seconds | Applied as both the connect timeout and the per-request timeout. |

The key travels in the `X-API-Key` header. Presigned download URLs are fetched without it, since they are already signed. Key types are covered in [Authentication](/docs/authentication); create and manage keys in the [dashboard](/dashboard).

<div class="alert alert-warning">
<strong>Never hardcode the API key.</strong> Read it from an environment variable or your secret manager. The SDK is server side only: a private key must not ship inside a desktop or mobile artifact that a user can unpack.
</div>

---

## Result shape

Every single file and single URL conversion returns the same record:

```java
public record ConversionResult(
        String presignedUrl,
        String objectKey,
        String filename,
        Long fileSize,
        Double conversionTimeSeconds,
        String jobId) {}
```

The presigned URL is time limited. Pass `saveTo` (or fetch the URL yourself) and store the bytes in your own bucket if you need them to outlive it.

The other response records you will touch most often:

| Record | Key accessors |
|--------|---------------|
| `JobStatus` | `status()` (`processing`, `success`, `failed`), `presignedUrl()`, `objectKey()`, `error()` |
| `BatchStatus` | `status()`, `total()`, `completed()`, `failed()`, `inProgress()`, `zipDownloadUrl()`, `items()` |
| `PerceiveResult` | `operationId()`, `renderQuality()`, `statusCode()`, `deductions()`, `outputs()`, `structured()`, `cacheHit()`, `warnings()` |
| `V2OutputArtifact` | `url()`, `objectKey()`, `sizeBytes()`, `contentType()`, `expiresIn()` |
| `PerceiveDirectResult` | `content()`, `contentType()`, `filename()`, `renderQuality()`, `contentHash()` |
| `IngestJob` | `jobId()`, `status()`, `pagesProcessed()`, `totalChunks()`, `outputUrl()`, `webhookDelivered()` |
| `Watcher` | `watcherId()`, `status()`, `frequencyMinutes()`, `checksCount()`, `nextCheckAt()`, `lastChangeAt()` |

Fields whose shape is defined by your own request (`structured`, `data`, `trackFields`, snapshot `changes`) are exposed as Gson `JsonObject` and pass through untouched. String valued enumerations stay `String` rather than becoming Java `enum` constants, so a newer API value never breaks deserialization on an older SDK build. `com.enconvert.model.v2.V2Enums` holds every accepted value as a typo-proof constant.

---

## Source and issues

- **Maven Central:** `com.enconvert:enconvert-sdk:0.0.1`
- **GitHub:** [conversionapi/java-sdk](https://github.com/conversionapi/java-sdk)
- **License:** MIT
- **Other clients:** [all SDKs](/docs/sdks) · [endpoint reference](/docs/endpoints-overview) · [pricing](/pricing)

---

## Frequently asked questions

### How do I convert files in Java with a Maven dependency?

Add `com.enconvert:enconvert-sdk:0.0.1` to your `pom.xml` or `build.gradle`, build a client with `new Enconvert(System.getenv("ENCONVERT_API_KEY"))`, and call a typed method such as `convertUrlToPdf`, `convertImage`, `convertDocument`, or `convertToPdf`. Pass `saveTo` on the options builder to write the output straight to disk instead of handling the presigned URL yourself.

### How do I convert a URL to PDF in Java?

Call `client.convertUrlToPdf(url, UrlToPdfOptions.builder().saveTo("page.pdf").build())`. Set `singlePage(false)` to paginate using `pdfOptions.pageSize` instead of producing one continuous page, and pass `auth`, `cookies`, or `headers` for a page behind a login.

### How do I convert DOCX to PDF in Java?

Call `client.convertDocument(Path.of("report.docx"), ConvertDocumentOptions.builder().saveTo("report.pdf").build())`. The output format defaults to `pdf`, so you only set `outputFormat` when you want something else, for example `yaml` from a `.json` input. For formats without a dedicated pair, such as EPUB or RTF, use `convertToPdf`.

### How do I convert HEIC to WebP in Java?

Call `client.convertImage(Path.of("photo.heic"), ConvertImageOptions.builder("webp").saveTo("photo.webp").build())`. The input format comes from the file extension and the output format is the required builder argument. `jpeg`, `png`, `svg`, `heic`, and `webp` all convert to each other, and `pdf` rasterizes to `jpeg`. There is no compress-in-place method on this client.

### How do I scrape a web page into clean Markdown from Java?

Two paths. `client.convertUrlToMarkdown(url, ...)` returns GitHub Flavored Markdown with YAML frontmatter as a downloadable file. `client.v2.perceive(url, PerceiveOptions.builder().outputs(List.of("markdown")).build())` returns the same content as an agent-ready artifact with a `renderQuality` score, extraction options, and cache control. Use perceive when a bad read must be detectable rather than silent.

### What is renderQuality and why does every read have one?

`renderQuality` is a score from 0.0 to 1.0 attached to every V2 render. A high score means the page rendered cleanly; a low score means something got in the way, such as a bot challenge, a cookie wall, a login screen, an HTTP error page, or an empty SPA shell. The content is still returned, with `warnings()` and `deductions()` populated, so your pipeline can drop or retry the read instead of feeding a challenge page into a model as if it were the article.

### How do I turn a documentation site into RAG-ready chunks in Java?

Call `client.v2.ingest(IngestOptions.builder().mode("sitemap").url("https://docs.example.com").maxPages(100).chunk(new IngestChunkOptions(512, 1)).build())`. The job is asynchronous, so either poll `getIngestJob(jobId)` until the status is `completed` and read `outputUrl()` for the JSONL, or set `webhookUrl` and verify the HMAC signature with the secret from `getWebhookSecret()`. For local documents rather than a site, use `ingestFiles`.

### How does the SDK handle conversions that outlive the proxy timeout?

Before each single URL or file upload request it generates a UUID and sends it as `job_id`. If the request returns 5xx, it polls `GET /v1/convert/status/{jobId}` every 3 seconds until the job reports `success` or `failed`, with a 5 minute deadline after which it throws `ApiException(504, "Conversion timed out")`. Whole site batch submissions are excluded, because they have no per-job row to poll.

### Which Java version does the SDK require, and what does it pull in?

Java 17 or newer. HTTP goes through the JDK's `java.net.http.HttpClient`, and Gson is the only third party artifact on the classpath. Responses are Java records, so a modern `switch` or pattern match over them works as expected. The client is thread safe: hold one instance and share it.
