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.

Maven Central: com.enconvert:enconvert-sdk:0.0.1 · Source: conversionapi/java-sdk · Java: 17+ · Dependencies: Gson only

Install#

// build.gradle
dependencies {
    implementation 'com.enconvert:enconvert-sdk:0.0.1'
}
// build.gradle.kts
dependencies {
    implementation("com.enconvert:enconvert-sdk:0.0.1")
}
<!-- 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 for JSON, declared as an api dependency so it is visible on your compile classpath.


Quick start#

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.

convertUrlToPdf#

Render any public URL to PDF.

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.
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.

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.

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.

// 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.

// 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 or 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.

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.

// 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.

Only grayscale is honored on this endpoint. Page geometry (page size, width and height, orientation, margins, scale, header, footer) is ignored by anything-to-pdf. When you need full page setup, go through convertDocument or convertUrlToPdf instead.

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.

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("[email protected]")
                .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#

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.

Perceive#

Render one URL into the artifacts you ask for. Endpoint reference: Perceive.

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.
Not wired up yet. proxyUrl, geolocation, and actionChain exist on the builder but are not available server side and are currently rejected with 422.

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.

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:

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.

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.

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.

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:

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.

// 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:

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.

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.

Snapshot diffs contain untrusted page content. WatcherSnapshot.changes() is raw text lifted from the monitored page. Escape it before rendering it in a dashboard, an email, or a chat message.

PDF options#

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

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.

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 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.

Website batch submissions are excluded on purpose. convertWebsiteToPdf and convertWebsiteToScreenshot 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 getPerceiveBatch and getIngestJob.

Configuration#

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; create and manage keys in the dashboard.

Never hardcode the API key. 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.

Result shape#

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

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#


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.