Kotlin File Conversion SDK#
com.enconvert:enconvert-kotlin is the official EnConvert client for Kotlin and the JVM, built against JDK 17. It converts files across 43 implemented format pairs (DOCX to PDF, HEIC to WebP, JSON to YAML, URL to PDF, anything to Markdown), and it reads live web pages into agent-ready Markdown, JSON, screenshots, and RAG-ready JSONL through the client.v2 namespace. Options and responses are idiomatic Kotlin data classes with named arguments and sane defaults, HTTP rides on the JDK's own java.net.http.HttpClient, and slow conversions recover from reverse-proxy timeouts by polling job status.
com.enconvert:enconvert-kotlin:0.0.1 · Source: conversionapi/kotlin-sdk · Requires: JDK 17+ · License: MIT
Install#
// Gradle, Kotlin DSL
dependencies {
implementation("com.enconvert:enconvert-kotlin:0.0.1")
}
// Gradle, Groovy DSL
dependencies {
implementation 'com.enconvert:enconvert-kotlin:0.0.1'
}
<dependency>
<groupId>com.enconvert</groupId>
<artifactId>enconvert-kotlin</artifactId>
<version>0.0.1</version>
</dependency>
The only runtime dependency is org.jetbrains.kotlinx:kotlinx-serialization-json. Everything else comes from the JDK: requests go out through java.net.http.HttpClient and multipart bodies are assembled by the SDK itself. The toolchain targets JVM 17, so any JDK 17 or newer runtime works.
Quick start#
import com.enconvert.Enconvert
import com.enconvert.PerceiveOptions
import com.enconvert.PerceiveOutputName
import com.enconvert.UrlToPdfOptions
fun main() {
val client = Enconvert(apiKey = System.getenv("ENCONVERT_API_KEY"))
// Convert a live page to PDF and stream it straight to disk.
val pdf = client.convertUrlToPdf("https://example.com", UrlToPdfOptions(saveTo = "page.pdf"))
println(pdf.presignedUrl)
// Read the same page the way your agent should, with a quality score attached.
val op = client.v2.perceive(
"https://example.com",
PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN, PerceiveOutputName.STRUCTURED)),
)
println("${op.outputs["markdown"]?.url} ${op.renderQuality}") // e.g. 0.93
}
Every EnConvert type lives in the com.enconvert package, and the snippets below leave imports out; JDK types such as java.nio.file.Files and java.nio.file.Path appear unqualified for the same reason. Every method blocks, since there are no suspend functions, so from a coroutine wrap the call in withContext(Dispatchers.IO). The client authenticates with a private API key sent as the X-API-Key header, which makes it server-side only: never ship the key inside an Android app or anything else you distribute. See Authentication for key types.
What the client exposes#
Enconvert is the whole surface. File conversion lives on the client itself; web intelligence lives on the v2 namespace reached as client.v2.
| Surface | Reached as | Covers |
|---|---|---|
| File conversion | client.<method>() |
URL to PDF, screenshot, Markdown; image and document pairs; anything-to-PDF and anything-to-Markdown; whole-site batches; job and batch status |
| Web intelligence | client.v2.<method>() |
Perceive, discover, lookup, distill, ingest, watch: 23 methods over 21 REST endpoints |
Twelve conversion methods map onto the REST API described in the endpoints overview:
| Method | Endpoint | Returns |
|---|---|---|
convertUrlToPdf(url, opts?) |
POST /v1/convert/url-to-pdf |
ConversionResult |
convertUrlToScreenshot(url, opts?) |
POST /v1/convert/url-to-screenshot |
ConversionResult |
convertUrlToMarkdown(url, opts?) |
POST /v1/convert/url-to-markdown |
ConversionResult |
convertImage(file, opts) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
convertDocument(file, opts?) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
convertToMarkdown(file, opts?) |
POST /v1/convert/anything-to-markdown |
ConversionResult |
convertToPdf(file, opts?) |
POST /v1/convert/anything-to-pdf |
ConversionResult |
convertWebsiteToPdf(url, opts?) |
POST /v1/convert/website-to-pdf |
BatchSubmission |
convertWebsiteToScreenshot(url, opts?) |
POST /v1/convert/website-to-screenshot |
BatchSubmission |
getJobStatus(jobId) |
GET /v1/convert/status/{jobId} |
JobStatus |
getBatchStatus(batchId) |
GET /v1/convert/batch/{batchId} |
BatchStatus |
waitForBatch(batchId, opts?) |
GET /v1/convert/batch/{batchId} (polled) |
BatchStatus |
The four file-upload methods each have four overloads. The first argument may be a path String, a java.nio.file.Path, a bare ByteArray (filename defaults to upload.bin), or a FileInput(data, filename, contentType?) when you have raw bytes and want to name them yourself.
File conversion#
convertUrlToPdf#
Render any public URL to PDF.
val result = client.convertUrlToPdf(
"https://example.com/report",
UrlToPdfOptions(
render = UrlRenderOptions(viewportWidth = 1440),
singlePage = false,
pdfOptions = PdfOptions(pageSize = "A4", orientation = PdfOrientation.LANDSCAPE, margins = PdfMargins(top = 10.0)),
saveTo = "report.pdf",
),
)
println("${result.filename} ${result.fileSize}")
| Option | Type | Default | Description |
|---|---|---|---|
render |
UrlRenderOptions |
UrlRenderOptions() |
Viewport, media, scroll, filename, browser access. |
saveTo |
String? |
-- | Local path to stream the PDF to. Parent directories are created for you. |
singlePage |
Boolean |
true |
true produces one continuous page. false paginates using pdfOptions.pageSize. |
pdfOptions |
PdfOptions? |
-- | Page geometry. See PDF options. |
UrlRenderOptions is shared by every URL-based conversion. It carries viewportWidth and viewportHeight (default 1920 x 1080), loadMedia and enableScroll (both default true, waiting for media and scrolling top to bottom so lazy loaders fire), outputFilename, and three browser-access fields: auth (an HttpBasicAuth), cookies (a List<BrowserCookie>, max 50), and headers (max 20, hop-by-hop headers rejected).
auth with an Authorization header. The API rejects the conflict rather than guessing which one you meant.
convertUrlToScreenshot#
Capture a PNG of any URL. UrlToScreenshotOptions carries render and saveTo only.
client.convertUrlToScreenshot(
"https://example.com",
UrlToScreenshotOptions(render = UrlRenderOptions(viewportWidth = 1440), saveTo = "shot.png"),
)
convertUrlToMarkdown#
Extract clean GitHub-Flavored Markdown from a URL. The converter strips navigation, footers, ads, and scripts, keeps the main article body, and prepends YAML frontmatter with the title, description, url, links, and images.
val article = client.convertUrlToMarkdown("https://example.com/post", UrlToMarkdownOptions(saveTo = "article.md"))
println(article.presignedUrl)
When you also want a quality score, an artifact bundle, or structured extraction from the same render, use client.v2.perceive instead.
convertImage#
Convert between jpeg, png, svg, heic, and webp in any direction, or rasterize a PDF to JPEG. ConvertImageOptions takes a required outputFormat plus optional saveTo and outputFilename.
client.convertImage("photo.heic", ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp"))
client.convertImage(Path.of("logo.svg"), ConvertImageOptions(outputFormat = "png", saveTo = "logo.png"))
val bytes = Files.readAllBytes(Path.of("photo.heic"))
client.convertImage(
FileInput(data = bytes, filename = "photo.heic"),
ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp"),
)
The input format is resolved from the filename extension. The output format is normalized for you, so "jpg" resolves to jpeg. Unsupported pairs throw EnconvertException before any network call, with the valid outputs listed in the message. You can ask the format table directly:
validOutputsFor("pdf") // [jpeg]
validOutputsFor("json") // [csv, toml, xml, yaml]
validOutputsFor("heic") // [jpeg, png, svg, webp]
convertDocument#
Convert documents and data formats. outputFormat defaults to "pdf".
client.convertDocument("report.docx", ConvertDocumentOptions(saveTo = "report.pdf"))
client.convertDocument("data.json", ConvertDocumentOptions(outputFormat = "yaml", saveTo = "data.yaml"))
client.convertDocument(
"README.md",
ConvertDocumentOptions(
outputFormat = "pdf",
pdfOptions = PdfOptions(pageSize = "A4", margins = PdfMargins(top = 20.0, bottom = 20.0)),
saveTo = "readme.pdf",
),
)
Supported inputs: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.
The 43 implemented pairs, exactly as the SDK gates them:
| Input | Outputs |
|---|---|
json |
csv, toml, xml, yaml |
xml |
csv, json |
csv |
json, xml |
yaml |
json |
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 |
EPUB has no dedicated document pair. Send .epub files through convertToPdf or convertToMarkdown. Options are outputFormat, saveTo, outputFilename, and pdfOptions (honored only when the output is PDF).
convertToMarkdown#
Convert an uploaded file of almost any document format to clean Markdown. The format is auto-detected server-side, so the SDK runs no extension check and uploads the file as-is.
client.convertToMarkdown("handbook.docx", ConvertToMarkdownOptions(saveTo = "handbook.md"))
Accepted: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy Office and ODF formats. Images are not supported here.
The output is one heading-aware .md file, which makes it a natural first stage for a RAG pipeline: a semantic chunker can split on the document's own heading hierarchy instead of arbitrary character counts. There are no PDF options on this endpoint; saveTo and outputFilename are the only options.
convertToPdf#
Convert an uploaded file of almost any format to PDF. Accepted input covers Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, and an existing PDF as a passthrough.
client.convertToPdf("slides.pptx", ConvertToPdfOptions(saveTo = "slides.pdf"))
client.convertToPdf("scan.pdf", ConvertToPdfOptions(pdfOptions = PdfOptions(grayscale = true), saveTo = "gray.pdf"))
pdfOptions.grayscale is honored on this endpoint. Page size, orientation, margins, scale, header, and footer are ignored here. When you need full page geometry, go through convertDocument or convertUrlToPdf instead.
convertWebsiteToPdf and convertWebsiteToScreenshot#
Discover every page of a website, convert each one in the background, and collect a single ZIP. Both are asynchronous: they return a BatchSubmission immediately, and you poll with getBatchStatus or block with waitForBatch.
val batch = client.convertWebsiteToPdf(
"https://example.com",
WebsiteToPdfOptions(
website = WebsiteConversionOptions(crawlMode = CrawlMode.SITEMAP, excludePatterns = listOf("/tag/")),
),
)
val status = client.waitForBatch(batch.batchId, WaitForBatchOptions(saveTo = "site.zip"))
println("${status.completed} of ${status.total} converted, ${status.failed} failed")
convertWebsiteToScreenshot works identically and produces a ZIP of PNGs. WebsiteConversionOptions carries render, crawlMode (AUTO, SITEMAP, FULL), includePatterns, excludePatterns, notificationEmail, and callbackUrl. waitForBatch polls every 5 seconds and gives up after 30 minutes, both overridable through WaitForBatchOptions(intervalMs, timeoutMs, saveTo); on timeout it throws ApiException with status 504.
getJobStatus#
Poll a single async or recovered conversion job.
val status = client.getJobStatus("job_abc123")
when (status.status) {
JobStatusValue.SUCCESS -> println(status.presignedUrl)
JobStatusValue.FAILED -> System.err.println(status.error)
JobStatusValue.PROCESSING -> println("still running")
}
Web intelligence (V2)#
Every V2 read carries renderQuality, a score from 0.0 to 1.0 describing how cleanly the page actually rendered. A challenge page, cookie wall, login gate, or empty SPA shell comes back with a low score, a populated deductions map naming which checks fired, and warnings, while the content itself is still returned. A bad read is flagged rather than quietly entering your agent's context. statusCode reports the HTTP status of the final main-document response, and contentHash lets you tell that nothing changed since the last read. Start at the V2 overview for the concepts behind the six capabilities.
| Capability | Methods on client.v2 |
|---|---|
| Perceive | perceive, getPerceiveOperation, perceiveBatch, getPerceiveBatch, perceiveDirect, downloadPerceiveArtifact |
| Discover | discover |
| Lookup | lookup |
| Distill | distill |
| Ingest | ingest, ingestFiles, listIngestJobs, getIngestJob, cancelIngestJob, retryIngestWebhook, getWebhookSecret, rotateWebhookSecret |
| Watch | createWatcher, listWatchers, getWatcher, getWatcherSnapshots, updateWatcher, deleteWatcher |
Perceive#
Render one URL into the artifacts you ask for. Synchronous: the call returns a completed operation whose artifact URLs are signed for 15 minutes. Full reference at Perceive.
val op = client.v2.perceive(
"https://example.com/pricing",
PerceiveOptions(
outputs = listOf(PerceiveOutputName.MARKDOWN, PerceiveOutputName.SCREENSHOT_FULL_PAGE, PerceiveOutputName.STRUCTURED),
extract = listOf(PerceiveExtractName.TABLES, PerceiveExtractName.METADATA),
viewport = PerceiveViewport(width = 1440),
waitFor = "css:.pricing-table",
),
)
if ((op.renderQuality ?: 0.0) < 0.5) System.err.println("Low quality read: ${op.deductions} ${op.warnings}")
println(op.outputs["markdown"]?.url)
println(op.structured)
| Option | Type | Default | Description |
|---|---|---|---|
outputs |
List<PerceiveOutputName>? |
[MARKDOWN, STRUCTURED] |
MARKDOWN, HTML_CLEANED, HTML_RAW, SCREENSHOT, SCREENSHOT_FULL_PAGE, PDF, LINKS, IMAGES, STRUCTURED. |
extract |
List<PerceiveExtractName>? |
-- | TABLES, PRICES, CONTACTS, METADATA, MAIN_CONTENT, HEADINGS, STRUCTURED_DATA, TECHNOLOGIES, ALL. |
schema |
Map<String, Any?>? |
-- | JSON schema for structured extraction. |
waitFor / waitTimeoutMs |
String? / Int? |
-- / 30000 |
A CSS selector (optionally css: prefixed) or js:<expr> to await, and its budget, 0 to 60000. |
jsCode |
String? |
-- | JavaScript run after navigation, max 20000 characters. |
viewport |
PerceiveViewport? |
1920 x 1080 | width 320-3840, height 240-2160. |
headers / cookies / auth |
-- | -- | Extra headers, injected cookies, HTTP Basic credentials. |
cacheMode |
PerceiveCacheMode? |
ENABLED |
ENABLED (1 hour cache), BYPASS, REFRESH. |
pdfOptions |
PdfOptions? |
-- | Only meaningful when outputs includes PDF. |
blockResources |
List<PerceiveResourceType>? |
-- | Resource types the browser should not load. |
respectRobots / mobile |
Boolean? |
-- | Honor robots.txt; render with a mobile profile. |
onlyMainContent |
Boolean? |
true |
Strip nav, header, footer, and cookie banners from the Markdown artifact and the main_content extract. |
directDownload |
Boolean? |
-- | Return artifact bytes instead of a JSON envelope. Prefer perceiveDirect. |
proxyUrl, geolocation, and actionChain exist on PerceiveOptions but are not yet available server-side and are currently rejected with 422.
Batch up to 1000 URLs behind one shared options block. Small batches complete inline; larger ones come back QUEUED, so poll the job id. getPerceiveOperation re-signs the artifact URLs of any earlier operation.
val batch = client.v2.perceiveBatch(
listOf("https://a.example.com", "https://b.example.com"),
PerceiveBatchOptions(
options = PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN)),
outputMode = PerceiveBatchOutputMode.ZIP,
),
)
var job = client.v2.getPerceiveBatch(batch.jobId)
while (job.status == PerceiveBatchStatus.QUEUED || job.status == PerceiveBatchStatus.PROCESSING) {
Thread.sleep(5_000)
job = client.v2.getPerceiveBatch(batch.jobId)
}
println("${job.completed}/${job.total} done, zip at ${job.zip?.url}")
val again = client.v2.getPerceiveOperation(op.operationId) // freshly signed URLs
When you want the bytes and nothing else, perceiveDirect streams the artifact back on the same request and skips the signed-URL round trip. It needs exactly one artifact-producing output, meaning anything except STRUCTURED, and throws EnconvertException locally if you ask for zero or more than one.
val direct = client.v2.perceiveDirect("https://example.com", PerceiveOptions(outputs = listOf(PerceiveOutputName.PDF)))
Files.write(Path.of(direct.filename ?: "page.pdf"), direct.content)
println("${direct.renderQuality} ${direct.sourceStatusCode} ${direct.warningsCount}")
// Re-download a stored artifact of an earlier operation.
val markdown = client.v2.downloadPerceiveArtifact(op.operationId, PerceiveOutputName.MARKDOWN)
println(markdown.content.toString(Charsets.UTF_8))
downloadPerceiveArtifact accepts a null output when the operation produced exactly one artifact, and returns 410 once the stored artifact passes its retention window.
Discover#
Enumerate a site's URLs with no browser rendering at all. Full reference at Discover.
val found = client.v2.discover(
"https://example.com",
DiscoverOptions(mode = DiscoverMode.HYBRID, maxUrls = 200, maxDepth = 3, excludePatterns = listOf("/tag/")),
)
println("${found.total} urls, truncated=${found.truncated}, sources=${found.sources}")
| Option | Type | Default | Description |
|---|---|---|---|
mode |
DiscoverMode? |
HYBRID |
SITEMAP, CRAWL, or HYBRID (sitemap plus HTTP crawl). |
maxUrls / maxDepth |
Int? |
100 / 2 |
1-1000 and 1-5. |
includePatterns / excludePatterns |
List<String>? |
-- | Regex allowlist and denylist, max 50 entries each. The denylist is applied second. |
sameDomainOnly |
Boolean? |
true |
Stay on the seed domain. |
respectRobots |
Boolean? |
-- | Honor robots.txt. |
DiscoverResult.sources reports raw per-source counts before dedup, for example {sitemap=42, crawl=30}.
Lookup#
Run a categorized web search, and optionally perceive the top results in the same call. Full reference at Lookup.
val search = client.v2.lookup(
"best static site generators",
LookupOptions(category = LookupCategory.WEB, numResults = 10, country = "us", timeFilter = LookupTimeFilter.MONTH, perceiveTop = 3),
)
for (hit in search.results) {
println("${hit.position}. ${hit.title} ${hit.url}")
hit.perceive?.let { println(" rendered at quality ${it.renderQuality}") }
}
| Option | Type | Default | Description |
|---|---|---|---|
category |
LookupCategory? |
WEB |
WEB, NEWS, IMAGES, SCHOLAR, PATENTS, MAPS. |
country / locale |
String? |
-- | Google gl country code and hl interface language. |
timeFilter |
LookupTimeFilter? |
-- | HOUR, DAY, WEEK, MONTH, YEAR. |
numResults / page |
Int? |
10 / 1 |
1-100 and 1-10. |
location |
String? |
-- | Free-text location, for example "Austin, Texas". |
autocorrect |
Boolean? |
true |
Let the provider fix typos. |
perceiveTop |
Int? |
0 |
Auto-perceive the top N result URLs, 0-10. Each one runs a full browser render. |
The result also carries answerBox, knowledgeGraph, perceiveOperationIds, and total.
Distill#
Point a schema at some pages and get structured data back. An optional CSS pass answers whatever it can before anything escalates to the LLM tier. Full reference at Distill.
val extraction = client.v2.distill(
DistillOptions(
urls = listOf("https://example.com/pricing"),
schema = mapOf("plans" to "list of plan names with monthly prices"),
cssSchema = CssSchema(
baseSelector = ".plan-card",
fields = listOf(
CssField(name = "name", type = CssFieldType.TEXT, selector = "h3"),
CssField(name = "price", type = CssFieldType.TEXT, selector = ".price"),
),
targetField = "plans",
),
),
)
for (item in extraction.results) {
println("${item.url} tier=${item.extractionTier} css=${item.fieldsFromCss} llm=${item.fieldsFromLlm}")
println(item.data)
}
Or discover the URLs first and distill each one:
client.v2.distill(
DistillOptions(
discoverFrom = DistillDiscoverFrom(url = "https://example.com", mode = DiscoverMode.SITEMAP, maxPages = 10),
schema = mapOf("title" to "page title", "summary" to "one-line summary"),
),
)
Provide exactly one of urls or discoverFrom. Passing both, or neither, throws EnconvertException before the request leaves your process.
| Option | Type | Default | Description |
|---|---|---|---|
urls |
List<String>? |
-- | Explicit URLs to distill, max 50. |
discoverFrom |
DistillDiscoverFrom? |
-- | Discover a site's URLs first. maxPages is 1-50, default 10. |
schema |
Map<String, Any?> |
required | A JSON Schema object, or a flat {field to description} map. |
cssSchema |
CssSchema? |
-- | CSS pass run before any LLM escalation. |
waitFor / waitTimeoutMs |
String? / Int? |
-- / 30000 |
Selector or js: expression to await, and its budget. |
headers / cookies / respectRobots |
-- | -- | Same render controls as perceive. |
CssField.type is one of TEXT, ATTRIBUTE, HTML, REGEX, NESTED, LIST, NESTED_LIST. ATTRIBUTE requires attribute, REGEX requires pattern, and the three nested kinds require a non-empty fields list, up to five levels deep.
Ingest#
Turn a whole site, or a pile of uploaded documents, into chunked RAG-ready JSONL through one pipeline. Ingest is always asynchronous. Full reference at Ingest.
val job = client.v2.ingest(
IngestOptions(
mode = IngestMode.SITEMAP,
url = "https://docs.example.com",
maxPages = 100,
chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1),
webhookUrl = "https://my.app/hooks/enconvert",
),
)
var state = client.v2.getIngestJob(job.jobId)
while (state.status !in setOf(IngestStatus.COMPLETED, IngestStatus.FAILED, IngestStatus.CANCELED)) {
Thread.sleep(10_000)
state = client.v2.getIngestJob(job.jobId)
}
if (state.status == IngestStatus.COMPLETED) println("${state.totalChunks} chunks at ${state.outputUrl}")
mode is URLS by default, which requires a non-empty urls list and rejects url. SITEMAP and CRAWL require a seed url and reject urls. The SDK enforces both rules locally and throws EnconvertException rather than sending a request that cannot succeed.
Uploaded files go through ingestFiles, which shares the same job lifecycle under mode FILES and accepts PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, and legacy Office and ODF documents.
val paths = listOf(Path.of("handbook.pdf"), Path.of("notes.docx"))
val fileJob = client.v2.ingestFiles(
paths.map { FileInput(data = Files.readAllBytes(it), filename = it.fileName.toString()) },
IngestFilesOptions(chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1)),
)
println(fileJob.jobId)
| Option | Type | Default | Description |
|---|---|---|---|
mode |
IngestMode? |
URLS |
URLS, SITEMAP, CRAWL, FILES. |
url / urls |
String? / List<String>? |
-- | Seed URL for SITEMAP and CRAWL; explicit URLs (max 1000) for URLS. |
maxPages / maxDepth |
Int? |
50 / 2 |
Discovery caps, 1-1000 and 1-5. |
sameDomainOnly |
Boolean? |
true |
Stay on the seed domain. |
includePatterns / excludePatterns / respectRobots |
-- | -- | Same discovery controls as discover. |
waitFor / waitTimeoutMs |
String? / Int? |
-- / 30000 |
Per-page render wait. |
chunk |
IngestChunkOptions? |
-- | maxWords 32-4000 (default 512), sentenceOverlap 0-10 (default 1). |
webhookUrl |
String? |
-- | Completion webhook, HMAC-signed. |
Job management and webhook plumbing:
client.v2.listIngestJobs(V2ListOptions(limit = 20)) // newest first
client.v2.cancelIngestJob(job.jobId) // idempotent
val secret = client.v2.getWebhookSecret()
println("${secret.signatureHeader} ${secret.signatureScheme} ${secret.replayToleranceSeconds}s")
client.v2.rotateWebhookSecret() // old signatures stop verifying immediately
client.v2.retryIngestWebhook(job.jobId) // re-deliver a completed job's webhook
Watch#
Re-render a URL on a fixed cadence and get notified when it changes. Full reference at Watch.
val watcher = client.v2.createWatcher(
"https://example.com/pricing",
WatchCreateOptions(
frequencyMinutes = 60,
diffMode = WatchDiffMode.AUTO,
webhookUrl = "https://my.app/hooks/changes",
notifyEmail = true,
),
)
client.v2.listWatchers(V2ListOptions(limit = 20))
for (snap in client.v2.getWatcherSnapshots(watcher.watcherId, SnapshotListOptions(limit = 10)).snapshots) {
println("${snap.checkedAt} changed=${snap.hasChanges} similarity=${snap.similarity}")
}
client.v2.updateWatcher(watcher.watcherId, WatcherUpdate(status = WatcherUpdateStatus.PAUSED))
client.v2.updateWatcher(watcher.watcherId, WatcherUpdate(webhookUrl = "")) // clears the webhook
client.v2.deleteWatcher(watcher.watcherId) // soft delete, idempotent
| Option | Type | Default | Description |
|---|---|---|---|
frequencyMinutes |
Int? |
60 |
Minutes between checks, 60-43200. The hourly floor is hard. |
diffMode |
WatchDiffMode? |
AUTO |
AUTO, TEXT, STRUCTURED, TABLES, METADATA. |
trackFields |
Map<String, Any?>? |
-- | Field or selector subset the diff engine should watch. |
webhookUrl |
String? |
-- | Change webhook, HMAC-signed with the same secret as ingest. |
notifyEmail |
Boolean? |
true |
Email the project owner on changes. |
updateWatcher requires at least one field and throws EnconvertException on an empty WatcherUpdate. Its status accepts only ACTIVE or PAUSED; deletion goes through deleteWatcher, which returns the tombstoned watcher with status DELETED. getWatcher on a deleted watcher reads as 404.
WatcherSnapshot.changes is raw text lifted from the watched page. Escape it before rendering it into HTML, a dashboard, or a chat message.
PDF options#
PdfOptions is shared by convertUrlToPdf, convertDocument, convertToPdf (grayscale only), convertWebsiteToPdf, and PerceiveOptions.pdfOptions.
client.convertUrlToPdf(
"https://example.com",
UrlToPdfOptions(
pdfOptions = PdfOptions(
pageSize = "A4",
orientation = PdfOrientation.LANDSCAPE,
margins = PdfMargins(top = 10.0, bottom = 10.0, left = 15.0, right = 15.0),
scale = 0.9,
header = PdfHeaderFooter(content = "Quarterly report", height = 12.0),
),
saveTo = "report.pdf",
),
)
| Field | Type | Description |
|---|---|---|
pageSize |
String? |
"A4", "A3", "Letter", "Legal", and so on. |
pageWidth / pageHeight |
Double? |
Custom geometry. Together they override pageSize. |
orientation |
PdfOrientation? |
PORTRAIT or LANDSCAPE. Defaults to portrait. |
margins |
PdfMargins? |
top, bottom, left, right, all optional doubles in mm. |
scale |
Double? |
Render scale, for example 0.9 for 90 percent. |
grayscale |
Boolean? |
Post-process the PDF to grayscale. |
header / footer |
PdfHeaderFooter? |
content (max 2000 characters) and height. |
Only fields you actually set are serialized onto the wire, so a partially filled PdfOptions never overrides a server default you did not touch. The full parameter matrix lives in Parameters and options.
Error handling#
Every failure is an EnconvertException or a subclass, so a single catch can be your backstop while specific subclasses handle the cases you care about.
try {
client.v2.perceive("https://example.com")
} catch (e: AuthenticationException) {
System.err.println("Invalid or missing API key")
} catch (e: QuotaException) {
System.err.println("Request rejected with 402")
} catch (e: RateLimitException) {
System.err.println("Too many requests, back off and retry")
} catch (e: ApiException) {
System.err.println("API error [${e.statusCode}]: ${e.message}")
} catch (e: EnconvertException) {
System.err.println("Client-side validation failed: ${e.message}")
}
| Class | Raised on | Status code |
|---|---|---|
AuthenticationException |
Invalid, missing, or revoked API key | 401, 403 |
QuotaException |
Raised on HTTP 402 | 402 |
RateLimitException |
Too many requests | 429 |
ApiException |
Any other 4xx or 5xx response | the actual code |
EnconvertException |
Base class, plus client-side validation such as an unsupported conversion pair or a malformed options object | -- |
ApiException exposes the raw statusCode property, and its message renders as [<statusCode>] <server message> with the server's detail or error field pulled out of the JSON body. Catch order matters: the three narrow classes all extend ApiException, which extends EnconvertException, so list them first. The message map is documented in Error codes.
Timeout recovery#
Long URL-to-PDF renders and large document conversions can outlive a 60 to 120 second reverse-proxy timeout even when the conversion succeeds on the server. The SDK handles that on the V1 conversion methods:
- Before each request it generates a 32-character hex job id and sends it as
job_idin the JSON body or as a multipart field. - If the request comes back 5xx, the SDK stops trusting the response and polls
GET /v1/convert/status/{job_id}every 3 seconds. A404while the job row is still being written means "keep waiting". - On
successthe SDK maps the payload to a normalConversionResult. Onfailedit throwsApiExceptioncarrying the server's error message. - The deadline is 5 minutes, after which it throws
ApiException(504, "Conversion timed out").
Successful responses that omit job_id (the synchronous URL path does this) get the client-generated id backfilled, so result.jobId is always something you can hand to getJobStatus. Two deliberate exceptions: convertWebsiteToPdf and convertWebsiteToScreenshot skip the fallback, because a website submission has no per-job row and a 5xx there means the submission itself failed. V2 methods skip it too, since each V2 endpoint has its own polling or webhook story.
Configuration#
val client = Enconvert(
apiKey = System.getenv("ENCONVERT_API_KEY"),
timeout = 300_000, // ms, 5 minutes
baseUrl = "https://api.enconvert.com", // override for a self-hosted gateway
)
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
String |
required | Private API key. A blank value throws IllegalArgumentException from the constructor. |
timeout |
Long |
300_000 |
Per-request timeout in milliseconds, applied to the underlying HttpRequest. |
baseUrl |
String |
https://api.enconvert.com |
API base URL. Trailing slashes are stripped. |
Result shape#
Every conversion method returns a ConversionResult:
public data class ConversionResult(
val presignedUrl: String,
val objectKey: String,
val filename: String,
val fileSize: Long? = null,
val conversionTimeSeconds: Double? = null,
val jobId: String? = null,
)
The presigned URL is a temporary signed link. Pass saveTo if you want the bytes on disk right away, or download the URL yourself and store the file in your own bucket for permanent access.
V2 reads return a PerceiveResult instead, which is where the honesty signals live:
val op = client.v2.perceive("https://example.com")
op.operationId // "per_..."
op.status // PerceiveStatus.COMPLETED
op.url // requested URL; op.urlFinal after redirects
op.renderQuality // Double?, 0.0 to 1.0
op.statusCode // Int?, HTTP status of the main document
op.deductions // Map<String, Double>, e.g. {http_error=0.7}. Empty on a clean render.
op.warnings // List<String>
op.cacheHit // Boolean; op.contentHash is the SHA-256 of the rendered content
op.outputs // Map<String, V2OutputArtifact> keyed by output name
op.structured // Map<String, Any?>?, present when extract or schema was used
op.extractionTier // HEURISTIC, CSS, or LLM
op.tokens // V2Tokens(input, output); op.costCents and op.durationMs alongside
Each V2OutputArtifact carries url, objectKey, sizeBytes, contentType, and expiresIn (900 seconds). Artifact URLs are re-signed on every getPerceiveOperation call, so store the operationId, not the URL. Untyped payloads (extraction schemas, extracted data, tracked fields, diff entries, search extras) cross the boundary as Map<String, Any?> and convert losslessly in both directions, so nothing you put into a schema is reshaped on its way out.
Source and issues#
- Maven Central:
com.enconvert:enconvert-kotlin:0.0.1 - GitHub: conversionapi/kotlin-sdk
- License: MIT
- Other languages: see the full SDK list
Frequently asked questions#
How do I convert files in Kotlin?#
Add com.enconvert:enconvert-kotlin:0.0.1 to your Gradle or Maven build, construct Enconvert(apiKey = System.getenv("ENCONVERT_API_KEY")), and call a typed method such as convertDocument, convertImage, or convertUrlToPdf. Pass saveTo in the options object to stream the output straight to a local file instead of downloading the presigned URL yourself.
How do I convert DOCX to PDF in Kotlin?#
Call client.convertDocument("report.docx", ConvertDocumentOptions(saveTo = "report.pdf")). The output format defaults to pdf, so you can leave outputFormat unset. The input format is resolved from the file extension, and .doc and .docx both map to the same conversion. For page geometry, pass a PdfOptions through ConvertDocumentOptions.pdfOptions.
How do I convert a URL to PDF in Kotlin?#
Call client.convertUrlToPdf(url, UrlToPdfOptions(saveTo = "page.pdf")). Set singlePage = false to paginate with pdfOptions.pageSize, and use UrlRenderOptions to change the viewport, disable media loading, or turn off the scroll pass that triggers lazy loaders.
How do I convert HEIC to WebP on the JVM?#
Call client.convertImage("photo.heic", ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp")). All 20 pairs among jpeg, png, svg, heic, and webp are implemented, plus pdf to jpeg rasterization. An unsupported pair throws EnconvertException before any network call, and validOutputsFor("heic") lists the valid targets up front.
How do I scrape a web page into Markdown from Kotlin?#
Two options. client.convertUrlToMarkdown(url, UrlToMarkdownOptions(saveTo = "page.md")) gives you a Markdown file with YAML frontmatter. client.v2.perceive(url, PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN))) gives you the same content plus renderQuality, deductions, warnings, and statusCode, which is what you want when an agent will read the result unattended.
What does renderQuality mean and when should I reject a page?#
renderQuality runs from 0.0 to 1.0 and describes how cleanly the page rendered, not how good the content is. Challenge pages, login walls, HTTP errors, and empty SPA shells push it down, and deductions names each check that fired, for example {http_error=0.7}. The content is always returned so you can inspect it. A common pattern is to treat anything below 0.5 as suspect and either re-request with cacheMode = PerceiveCacheMode.REFRESH or route it to a human.
How do I turn a documentation site into RAG chunks from Kotlin?#
Call client.v2.ingest(IngestOptions(mode = IngestMode.SITEMAP, url = "https://docs.example.com", chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1))). Ingest is always asynchronous: poll getIngestJob(jobId) until the status is COMPLETED and read outputUrl for the JSONL, or set webhookUrl and let the completion webhook find you. Local documents go through ingestFiles with the same chunk settings.
Does the SDK block the calling thread?#
Yes. Every method calls HttpClient.send synchronously, and there are no suspend functions or coroutine builders in the SDK. waitForBatch and the internal timeout-recovery poller sleep the current thread between attempts. From a coroutine, wrap calls in withContext(Dispatchers.IO); from a server framework, keep them off the request-handling thread pool.
Can I call this SDK from Java?#
You can, since these are ordinary JVM classes, but Kotlin default arguments are not exposed to Java as overloads, so a Java caller has to pass every constructor argument of an options data class. If your codebase is Java, use the separate Java SDK listed on the SDK page instead.
Where do I get an API key?#
Create a private key in your dashboard. It is sent as the X-API-Key header on every request, so keep it server-side. Key types and scopes are covered in Authentication, and pricing covers the commercial side.